From 4d3cb659c18100f5ca1c2ad2bd036b0ab2dc3ed0 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 11 Oct 2026 22:08:10 +0700 Subject: [PATCH] =?UTF-8?q?fix(wireguard):=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=B0=D1=82=D1=8C=20IPv6=20=D0=B8=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=BB=D1=8E=D1=87=D0=B8=D1=82=D1=8C=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 4 +- backend/src/routes/wireguard.ts | 30 +- backend/src/services/entity-snapshots.ts | 58 +- .../services/network-disabled-server.test.ts | 56 + backend/src/services/wireguard-config.ts | 8 +- backend/src/services/wireguard-delete.test.ts | 7 +- backend/src/services/wireguard-delete.ts | 15 +- .../services/wireguard-import-apply.test.ts | 33 + .../src/services/wireguard-import-apply.ts | 17 +- backend/src/services/wireguard-ipv6.test.ts | 52 + backend/src/services/wireguard-live.ts | 44 +- backend/src/services/wireguard-ros.ts | 4 +- components/wireguard/wg-create-sheet.tsx | 4 +- docs/ux-ui-external-acceptance.md | 24 +- docs/ux-ui-plan.md | 6 +- docs/ux-ui-progress.md | 24 +- docs/ux-ui-remaining-acceptance.md | 28 +- docs/ux-ui-stage-12.md | 22 +- docs/ux-ui-test-server.md | 24 +- .../network-disabled-sql-2026-10-11.json | 12 + ...wireguard-ipv6-api-history-2026-10-11.json | 185 ++++ ...guard-ipv6-before-gate-api-2026-10-11.json | 179 ++++ ...d-ipv6-before-gate-cleanup-2026-10-11.json | 49 + .../wireguard-ipv6-compiled-2026-10-11.json | 994 ++++++++++++++++++ ...wireguard-ipv6-initial-api-2026-10-11.json | 18 + ...guard-ipv6-initial-cleanup-2026-10-11.json | 49 + .../wireguard-ipv6-native-2026-10-11.json | 48 + .../wireguard-ipv6-smoke-2026-10-11.json | 49 + packages/contracts/src/wireguard.ts | 4 +- tests/e2e/wireguard-ipv6.spec.ts | 36 + 30 files changed, 2001 insertions(+), 82 deletions(-) create mode 100644 backend/src/services/network-disabled-server.test.ts create mode 100644 backend/src/services/wireguard-ipv6.test.ts create mode 100644 docs/ux-ui/acceptance/network-disabled-sql-2026-10-11.json create mode 100644 docs/ux-ui/acceptance/wireguard-ipv6-api-history-2026-10-11.json create mode 100644 docs/ux-ui/acceptance/wireguard-ipv6-before-gate-api-2026-10-11.json create mode 100644 docs/ux-ui/acceptance/wireguard-ipv6-before-gate-cleanup-2026-10-11.json create mode 100644 docs/ux-ui/acceptance/wireguard-ipv6-compiled-2026-10-11.json create mode 100644 docs/ux-ui/acceptance/wireguard-ipv6-initial-api-2026-10-11.json create mode 100644 docs/ux-ui/acceptance/wireguard-ipv6-initial-cleanup-2026-10-11.json create mode 100644 docs/ux-ui/acceptance/wireguard-ipv6-native-2026-10-11.json create mode 100644 docs/ux-ui/acceptance/wireguard-ipv6-smoke-2026-10-11.json create mode 100644 tests/e2e/wireguard-ipv6.spec.ts diff --git a/backend/package.json b/backend/package.json index add1c91e..d15f4019 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,13 +18,13 @@ "test:probes": "tsx src/routes/probes.test.ts && tsx src/routes/probes.cancel.test.ts", "test:exec": "tsx src/services/mikrotik-logs.test.ts && tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts", "test:auth": "tsx src/lib/permissions.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts", - "test:wireguard": "npx tsx src/services/wireguard-config.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts && npx tsx src/routes/wireguard-import.test.ts", + "test:wireguard": "npx tsx src/services/wireguard-config.test.ts && npx tsx src/services/wireguard-ipv6.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts src/services/wireguard-delete.test.ts && npx tsx src/routes/wireguard-import.test.ts", "test:traffic-rate": "tsx src/services/traffic-rate.test.ts", "test:traffic-flow": "tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts", "test:users": "tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts", "test:pg": "tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/services/config-revisions.test.ts", "test:recursive-routes": "tsx src/routes/recursive-routes.test.ts && tsx src/services/recursive-route-catalog.test.ts && tsx src/services/recursive-apply.test.ts", - "test:config-sync": "tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts && tsx src/services/gre-ipv6.test.ts && tsx src/routes/gre-ipv6-validation.test.ts", + "test:config-sync": "tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts && tsx src/services/gre-ipv6.test.ts && tsx src/routes/gre-ipv6-validation.test.ts && tsx src/services/network-disabled-server.test.ts", "test:ros-file": "tsx src/services/ros-file-contents.test.ts && tsx src/services/mikrotik-response.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", diff --git a/backend/src/routes/wireguard.ts b/backend/src/routes/wireguard.ts index 814d8a3e..0aceba99 100644 --- a/backend/src/routes/wireguard.ts +++ b/backend/src/routes/wireguard.ts @@ -39,7 +39,7 @@ import { loadRevisionForRestore, type ConfigRevisionSource, } from "../services/config-revisions.js" -import { parseWireguardSnapshot, planWireguardRestore } from "../services/entity-snapshots.js" +import { parseWireguardSnapshot, planWireguardRestore, planStaticInterfaceAddresses, interfaceAddressPath, InterfaceAddressConflict } from "../services/entity-snapshots.js" import { executeRosOps } from "../services/ros-ops.js" import { parseDbServerId } from "../utils/server-id.js" import { z } from "zod" @@ -190,6 +190,17 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => { const client = MikrotikClient.fromServer(server) const d = parsed.data try { + let addressOps: Parameters[1] = [] + if (d.address !== undefined) { + const state = await fetchWireguardRestoreState(server) + const live = state.ifaces.find(i => i.rosId === rosIdParam(rosId)) + if (!live) return reply.status(404).send({ error: "Интерфейс не найден" }) + if (d.name && d.name !== live.name) return reply.status(409).send({ error: "Изменяйте имя и адрес WireGuard отдельными операциями" }) + const original = state.snapshot.interfaces.find(i => i.name === live.name)! + const desired = original.innerAddresses!.filter(a => a.address !== original.address).map(a => ({ ...a })) + if (d.address && !desired.some(a => a.address === d.address)) desired.push({ address: d.address, disabled: false, ...(interfaceAddressPath(d.address) === "/ipv6/address" ? { advertise: false } : {}) }) + addressOps = planStaticInterfaceAddresses(live.name, d.address, desired, state.addrs) + } await client.patch( `/interface/wireguard/${encodeURIComponent(rosIdParam(rosId))}`, toRosBody({ @@ -201,11 +212,12 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => { disabled: d.disabled === true ? "yes" : d.disabled === false ? "no" : undefined, }), ) + await executeRosOps(client, addressOps) await recordWireguard(server, "apply") return reply.send({ ok: true }) } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return reply.status(502).send({ error: `RouterOS: ${msg}` }) + return reply.status(e instanceof InterfaceAddressConflict ? 409 : 502).send({ error: `RouterOS: ${msg}` }) } }) @@ -220,7 +232,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => { return reply.send({ ok: true }) } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return reply.status(502).send({ error: `RouterOS: ${msg}` }) + return reply.status(e instanceof InterfaceAddressConflict ? 409 : 502).send({ error: `RouterOS: ${msg}` }) } }) @@ -344,6 +356,16 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => { const iface = await findIface(list.interfaces, String(server.id), body.interfaceName) if (!iface) return reply.status(404).send({ error: "Интерфейс не найден" }) + if (body.format === "rsc" || body.format === "conf") { + const state = await fetchWireguardRestoreState(server) + const addresses = state.snapshot.interfaces.find(i => i.name === iface.name)?.innerAddresses + if (!addresses) return reply.status(409).send({ error: "Интерфейс изменился. Обновите список перед экспортом" }) + if (addresses.length > 1 || addresses.some(a => a.disabled || a.advertise || a.noDad || a.fromPool || a.eui64)) { + return reply.status(409).send({ error: "Экспорт не воспроизводит несколько адресов или специальные параметры. Используйте экспорт RouterOS" }) + } + if ((addresses[0]?.address ?? "") !== (iface.address ?? "")) return reply.status(409).send({ error: "Адрес изменился. Обновите список перед экспортом" }) + } + if (body.format === "rsc") { const content = generateMikrotikRsc({ name: iface.name, @@ -465,7 +487,7 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => { return reply.send({ ok: true, interfaces: list.interfaces }) } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return reply.status(502).send({ error: `RouterOS: ${msg}` }) + return reply.status(e instanceof InterfaceAddressConflict ? 409 : 502).send({ error: `RouterOS: ${msg}` }) } }) } diff --git a/backend/src/services/entity-snapshots.ts b/backend/src/services/entity-snapshots.ts index 67809b89..72a5960a 100644 --- a/backend/src/services/entity-snapshots.ts +++ b/backend/src/services/entity-snapshots.ts @@ -83,6 +83,7 @@ export interface WgSnapshotIface { mtu: number privateKey: string address: string + innerAddresses?: GreSnapshotAddress[] comment: string disabled: boolean peers: WgSnapshotPeer[] @@ -119,11 +120,7 @@ export interface WgLivePeer { clientEndpoint: string } -export interface WgLiveAddr { - rosId: string - interfaceName: string - address: string -} +export interface WgLiveAddr extends GreLiveAddr {} export interface GreSnapshotTunnel { name: string @@ -184,13 +181,14 @@ export interface GreSnapshotAddress { eui64?: boolean } -export class GreRestoreConflict extends Error {} +export class InterfaceAddressConflict extends Error {} +export const GreRestoreConflict = InterfaceAddressConflict export function greAddressPath(address: string): "/ip/address" | "/ipv6/address" { const parts = address.split("/") const family = isIP(parts[0] ?? "") if (!family || address.includes("%") || parts.length !== 2 || !/^\d+$/.test(parts[1] ?? "") || Number(parts[1]) > (family === 6 ? 128 : 32)) { - throw new GreRestoreConflict("Некорректный внутренний адрес GRE: требуется IPv4/IPv6 с префиксом") + throw new InterfaceAddressConflict("Некорректный адрес интерфейса: требуется IPv4/IPv6 с префиксом") } return family === 6 ? "/ipv6/address" : "/ip/address" } @@ -201,6 +199,9 @@ export function greAddressKey(address: string): string { return `${path === "/ipv6/address" ? new URL(`http://[${ip}]`).hostname : ip}/${Number(prefix)}` } +export const interfaceAddressPath = greAddressPath +export const interfaceAddressKey = greAddressKey + // ── IPsec / IKEv2 (managed-объекты, маркер MikrotikManager:ipsec) ──────────── export interface IpsecSnapshotPeer { @@ -520,6 +521,7 @@ export function canonicalWireguardSnapshot(input: { mtu: num(iface.mtu, 1420), privateKey: str(iface.privateKey), address: str(iface.address), + ...(iface.innerAddresses === undefined ? {} : { innerAddresses: canonicalGreSnapshot({ tunnels: [{ name: iface.name, innerAddresses: iface.innerAddresses }] }).tunnels[0]?.innerAddresses ?? [] }), comment: str(iface.comment), disabled: Boolean(iface.disabled), peers: (iface.peers ?? []).map(canonicalPeer).sort((a, b) => a.publicKey.localeCompare(b.publicKey)), @@ -576,8 +578,8 @@ export function planWireguardRestore( } for (const addr of current.addrs) { - if (!wantedNames.has(addr.interfaceName)) { - ops.push({ op: "delete", path: `/ip/address/${addr.rosId}` }) + if (!wantedNames.has(addr.interfaceName) && !addr.dynamic) { + ops.push({ op: "delete", path: `${interfaceAddressPath(addr.address)}/${addr.rosId}` }) } } @@ -607,17 +609,7 @@ export function planWireguardRestore( }) } - const liveAddr = current.addrs.find((a) => a.interfaceName === want.name) - if (want.address) { - if (!liveAddr) { - ops.push({ op: "put", path: "/ip/address", body: { address: want.address, interface: want.name } }) - } else if (liveAddr.address !== want.address) { - ops.push({ op: "delete", path: `/ip/address/${liveAddr.rosId}` }) - ops.push({ op: "put", path: "/ip/address", body: { address: want.address, interface: want.name } }) - } - } else if (liveAddr) { - ops.push({ op: "delete", path: `/ip/address/${liveAddr.rosId}` }) - } + ops.push(...planStaticInterfaceAddresses(want.name, want.address, want.innerAddresses, current.addrs)) for (const peer of want.peers) { if (!peer.publicKey) continue @@ -1014,26 +1006,32 @@ export function planGreRestore( ops.push({ op: "patch", path: `/interface/gre/${live.rosId}`, body }) } - const liveAddrs = current.addrs.filter(a => a.interfaceName === want.name && !a.dynamic) - if (liveAddrs.some(a => a.fromPool || a.eui64)) throw new GreRestoreConflict("GRE содержит адрес из IPv6-пула или EUI-64. Измените его отдельно в RouterOS") - if (want.innerAddresses === undefined && liveAddrs.length > 1) { - throw new GreRestoreConflict("Старая ревизия GRE не описывает все адреса интерфейса. Восстановление остановлено") + ops.push(...planStaticInterfaceAddresses(want.name, want.localInnerIp, want.innerAddresses, current.addrs)) + } + + return ops +} + +export function planStaticInterfaceAddresses(name: string, primary: string, desired: GreSnapshotAddress[] | undefined, current: GreLiveAddr[]): RosWriteOp[] { + const ops: RosWriteOp[] = [] + const liveAddrs = current.filter(a => a.interfaceName === name && !a.dynamic) + if (liveAddrs.some(a => a.fromPool || a.eui64)) throw new InterfaceAddressConflict("Интерфейс содержит адрес из IPv6-пула или EUI-64. Измените его отдельно в RouterOS") + if (desired === undefined && liveAddrs.length > 1) { + throw new InterfaceAddressConflict("Старая ревизия не описывает все адреса интерфейса. Восстановление остановлено") } - const addresses = want.innerAddresses ?? (want.localInnerIp ? [{ address: want.localInnerIp, disabled: false }] : []) + const addresses = desired ?? (primary ? [{ address: primary, disabled: false }] : []) const keys = new Set(addresses.map(a => greAddressKey(a.address))) - if (keys.size !== addresses.length) throw new GreRestoreConflict("В ревизии GRE повторяются внутренние адреса") + if (keys.size !== addresses.length) throw new InterfaceAddressConflict("В ревизии повторяются внутренние адреса") for (const addr of liveAddrs) { if (!keys.has(greAddressKey(addr.address))) ops.push({ op: "delete", path: `${greAddressPath(addr.address)}/${addr.rosId}` }) } for (const addr of addresses) { const existing = liveAddrs.find(a => greAddressKey(a.address) === greAddressKey(addr.address)) - if (!existing) ops.push({ op: "put", path: greAddressPath(addr.address), body: greAddressBody(addr, want.name) }) + if (!existing) ops.push({ op: "put", path: greAddressPath(addr.address), body: greAddressBody(addr, name) }) else if (Boolean(existing.disabled) !== addr.disabled || (addr.comment !== undefined && str(existing.comment) !== addr.comment) || (greAddressPath(addr.address) === "/ipv6/address" && (Boolean(existing.advertise) !== Boolean(addr.advertise) || (addr.noDad !== undefined && Boolean(existing.noDad) !== addr.noDad)))) { - ops.push({ op: "patch", path: `${greAddressPath(addr.address)}/${existing.rosId}`, body: greAddressBody(addr, want.name) }) + ops.push({ op: "patch", path: `${greAddressPath(addr.address)}/${existing.rosId}`, body: greAddressBody(addr, name) }) } } - } - return ops } diff --git a/backend/src/services/network-disabled-server.test.ts b/backend/src/services/network-disabled-server.test.ts new file mode 100644 index 00000000..a6c2e3b4 --- /dev/null +++ b/backend/src/services/network-disabled-server.test.ts @@ -0,0 +1,56 @@ +import assert from "node:assert/strict" +import { getEnabledServerById } from "./wireguard-live.js" +import { MikrotikClient } from "./mikrotik.js" +import { pool, closePool } from "../db/index.js" +import { initDatabase } from "../db/bootstrap.js" +import { buildApp } from "../index.js" + +try { + for (const id of ["srv1", "1abc", "", "0", "-1", "1.5", "9007199254740993", "2147483648"]) assert.equal(await getEnabledServerById(id), null) + const own = process.env.MMAPP_OWN_TEST_DB + if (own) { + assert(/^mm_accept_152_\d+$/.test(own)) + assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name, own) + await initDatabase() + const id = Number((await pool.query("INSERT INTO servers (name,host,enabled) VALUES ('disabled-network-fixture','127.0.0.1',false) RETURNING id")).rows[0].id) + const app = await buildApp({ logger: false, startScheduler: false }) + const original = MikrotikClient.fromServer + let contacts = 0, checks = 0 + try { + MikrotikClient.fromServer = () => { contacts++; throw new Error("Disabled server was contacted") } + const key = Buffer.alloc(32, 7).toString("base64") + for (const type of ["home-router", "jump-host", "exit-node"]) { + await pool.query("UPDATE servers SET type=$1 WHERE id=$2", [type, id]) + assert.equal(await getEnabledServerById(id), null) + for (const path of ["/wireguard", "/gre/tunnels"]) { + const r = await app.inject({ method: "GET", url: `/api${path}?serverId=${id}` }) + assert.equal(r.statusCode, 200) + assert.deepEqual(r.json().failures, []) + assert.deepEqual(path === "/wireguard" ? r.json().interfaces : r.json().tunnels, []) + checks++ + } + const cases = [ + { method: "POST", path: "/wireguard/interfaces", payload: { serverId: id, name: "own", address: "fd00::1/64" } }, + { method: "PATCH", path: `/wireguard/interfaces/${id}/*none`, payload: { address: "fd00::1/64" } }, + { method: "DELETE", path: `/wireguard/interfaces/${id}/*none`, payload: undefined }, + { method: "POST", path: "/wireguard/peers", payload: { serverId: id, interfaceName: "own", publicKey: key, allowedAddresses: ["fd00::2/128"] } }, + { method: "POST", path: "/wireguard/export", payload: { serverId: id, interfaceName: "own", format: "rsc" } }, + { method: "POST", path: "/wireguard/import", payload: { serverId: id, format: "conf", content: `[Interface]\nPrivateKey = ${key}\nAddress = fd00::1/64\n` } }, + { method: "POST", path: "/gre/tunnels", payload: { serverId: String(id), name: "own", remoteAddress: "192.0.2.1", localInnerIp: "fd00::1/64" } }, + { method: "PATCH", path: "/gre/tunnels", payload: { serverId: String(id), name: "own", localInnerIp: "fd00::1/64" } }, + { method: "DELETE", path: "/gre/tunnels", payload: { serverId: String(id), name: "own" } }, + ] as const + for (const item of cases) { + const r = await app.inject({ method: item.method, url: "/api" + item.path, payload: item.payload }) + assert.equal(r.statusCode, 404, type + " " + item.path + " " + r.body) + checks++ + } + } + assert.equal(checks, 33) + assert.equal(contacts, 0) + await pool.query("UPDATE servers SET enabled=true WHERE id=$1", [id]) + assert.equal((await getEnabledServerById(id))?.id, id) + console.log("network-disabled-server.test.ts: 33 API/SQL checks for three types, zero RouterOS contacts, enabled positive control passed") + } finally { MikrotikClient.fromServer = original; await app.close() } + } else console.log("network-disabled-server.test.ts: invalid IDs passed; own SQL/disabled API checks skipped (MMAPP_OWN_TEST_DB required)") +} finally { await closePool() } diff --git a/backend/src/services/wireguard-config.ts b/backend/src/services/wireguard-config.ts index c69df533..7b35912b 100644 --- a/backend/src/services/wireguard-config.ts +++ b/backend/src/services/wireguard-config.ts @@ -225,8 +225,11 @@ export function parseMikrotikRsc(content: string): WgParsedConfig { continue } - if (lower.startsWith("/ip address add") || lower.startsWith("/ip/address add")) { + if (lower.startsWith("/ip address add") || lower.startsWith("/ip/address add") || lower.startsWith("/ipv6 address add") || lower.startsWith("/ipv6/address add")) { const kv = parseKvLine(line) + if (iface.address) throw new Error("Импорт нескольких адресов пока не поддерживается") + if (kv.interface && kv.interface !== iface.name) throw new Error("Адрес относится к другому интерфейсу") + if (kv["from-pool"] || kv["eui-64"] === "yes" || kv["no-dad"] === "yes" || kv.disabled === "yes" || kv.advertise === "yes") throw new Error("Импорт специальных параметров адреса не поддерживается") if (kv.address) iface.address = kv.address continue } @@ -327,8 +330,9 @@ export function generateMikrotikRsc(iface: WgExportIface): string { lines.push(``) if (iface.address) { - lines.push(`/ip address add \\`) + lines.push(`${iface.address.includes(":") ? "/ipv6" : "/ip"} address add \\`) lines.push(` address=${iface.address} \\`) + if (iface.address.includes(":")) lines.push(` advertise=no \\`) lines.push(` interface=${iface.name}`) lines.push(``) } diff --git a/backend/src/services/wireguard-delete.test.ts b/backend/src/services/wireguard-delete.test.ts index 6bd6c27b..b7ffb1e1 100644 --- a/backend/src/services/wireguard-delete.test.ts +++ b/backend/src/services/wireguard-delete.test.ts @@ -8,6 +8,7 @@ function fixture(opts: { failRead?: string; failDelete?: string; keep?: string; '/interface/wireguard': [{ '.id': '*A/1', name: 'wg-own' }, { '.id': '*B', name: 'wg-foreign' }], '/interface/wireguard/peers': [{ '.id': '*P', interface: 'wg-own' }, { '.id': '*Q', interface: '*A/1' }, { '.id': '*F', interface: 'wg-foreign' }], '/ip/address': [{ '.id': '*I', interface: 'wg-own' }, { '.id': '*J', interface: 'wg-foreign' }], + '/ipv6/address': [{ '.id': '*I', interface: 'wg-own' }, { '.id': '*J', interface: 'wg-foreign' }, { '.id': '*D', interface: 'wg-own', dynamic: 'true' } as Row], } if (opts.missing) rows['/interface/wireguard'] = rows['/interface/wireguard'].filter(r => r.name !== 'wg-own') const deletes: string[] = [] @@ -28,12 +29,12 @@ function fixture(opts: { failRead?: string; failDelete?: string; keep?: string; test('removes named/id-associated peers and addresses before interface; preserves foreign objects', async () => { const f = fixture(); await deleteWireguardInterface(f.client, '*A/1') - assert.deepEqual(f.deletes, ['/interface/wireguard/peers/*P', '/interface/wireguard/peers/*Q', '/ip/address/*I', '/interface/wireguard/*A%2F1']) + assert.deepEqual(f.deletes, ['/interface/wireguard/peers/*P', '/interface/wireguard/peers/*Q', '/ip/address/*I', '/ipv6/address/*I', '/interface/wireguard/*A%2F1']) assert.deepEqual(f.rows['/interface/wireguard'], [{ '.id': '*B', name: 'wg-foreign' }]) assert.deepEqual(f.rows['/interface/wireguard/peers'], [{ '.id': '*F', interface: 'wg-foreign' }]) assert.deepEqual(f.rows['/ip/address'], [{ '.id': '*J', interface: 'wg-foreign' }]) }) -for (const path of ['/interface/wireguard', '/interface/wireguard/peers', '/ip/address']) { +for (const path of ['/interface/wireguard', '/interface/wireguard/peers', '/ip/address', '/ipv6/address']) { test(`read failure ${path} prevents every write`, async () => { const f = fixture({ failRead: path }); await assert.rejects(deleteWireguardInterface(f.client, '*A/1'), /Read forbidden/); assert.deepEqual(f.deletes, []) }) @@ -47,7 +48,7 @@ test('partial failure is surfaced and interface remains available for retry', as await deleteWireguardInterface({ ...f.client, async delete(path) { const split = path.lastIndexOf('/'), base = path.slice(0, split), id = decodeURIComponent(path.slice(split + 1)); f.rows[base] = f.rows[base].filter(r => r['.id'] !== id) } }, '*A/1') assert.deepEqual(f.rows['/ip/address'], [{ '.id': '*J', interface: 'wg-foreign' }]) }) -for (const path of ['/interface/wireguard', '/interface/wireguard/peers', '/ip/address']) { +for (const path of ['/interface/wireguard', '/interface/wireguard/peers', '/ip/address', '/ipv6/address']) { test(`unconfirmed removal ${path} cannot report success`, async () => { const f = fixture({ keep: path }); await assert.rejects(deleteWireguardInterface(f.client, '*A/1'), /не подтвердил/) }) diff --git a/backend/src/services/wireguard-delete.ts b/backend/src/services/wireguard-delete.ts index 1a9b76fa..9bb4fed7 100644 --- a/backend/src/services/wireguard-delete.ts +++ b/backend/src/services/wireguard-delete.ts @@ -1,6 +1,6 @@ import type { MikrotikClient } from "./mikrotik.js" -type RosRow = { ".id"?: string; name?: string; interface?: string } +type RosRow = { ".id"?: string; name?: string; interface?: string; dynamic?: string } type Client = Pick /** RouterOS retains peer/address records when their WireGuard interface is removed. */ @@ -10,13 +10,17 @@ export async function deleteWireguardInterface(client: Client, rosId: string): P if (!target?.name) throw new Error("Интерфейс WireGuard не найден; удаление не выполнено") const name = target.name const belongs = (row: RosRow) => row.interface === name || row.interface === rosId - const [peers, addresses] = await Promise.all([ + const [peers, addresses, ipv6Addresses] = await Promise.all([ client.get("/interface/wireguard/peers"), client.get("/ip/address"), + client.get("/ipv6/address"), ]) + if (![peers, addresses, ipv6Addresses].every(Array.isArray)) throw new Error("Не удалось прочитать все связанные объекты WireGuard") + const staticAddress = (row: RosRow) => row.dynamic !== "true" && row.dynamic !== "yes" const groups = [ { path: "/interface/wireguard/peers", rows: peers.filter(belongs) }, { path: "/ip/address", rows: addresses.filter(belongs) }, + { path: "/ipv6/address", rows: ipv6Addresses.filter(staticAddress).filter(belongs) }, ] // Finish all reads and validate identities before the first destructive request. if (groups.some(group => group.rows.some(row => !row[".id"]))) { @@ -26,15 +30,18 @@ export async function deleteWireguardInterface(client: Client, rosId: string): P for (const row of group.rows) await client.delete(`${group.path}/${encodeURIComponent(row[".id"]!)}`) } await client.delete(`/interface/wireguard/${encodeURIComponent(rosId)}`) - const [remainingInterfaces, remainingPeers, remainingAddresses] = await Promise.all([ + const [remainingInterfaces, remainingPeers, remainingAddresses, remainingIpv6Addresses] = await Promise.all([ client.get("/interface/wireguard"), client.get("/interface/wireguard/peers"), client.get("/ip/address"), + client.get("/ipv6/address"), ]) + if (![remainingInterfaces, remainingPeers, remainingAddresses, remainingIpv6Addresses].every(Array.isArray)) throw new Error("RouterOS не подтвердил полное удаление WireGuard") const hasRemaining = (rows: RosRow[], original: RosRow[]) => rows.some(row => belongs(row) || original.some(before => before[".id"] === row[".id"])) if (remainingInterfaces.some(row => row[".id"] === rosId || row.name === name) || hasRemaining(remainingPeers, groups[0].rows) - || hasRemaining(remainingAddresses, groups[1].rows)) { + || hasRemaining(remainingAddresses, groups[1].rows) + || hasRemaining(remainingIpv6Addresses.filter(staticAddress), groups[2].rows)) { throw new Error("RouterOS не подтвердил полное удаление интерфейса WireGuard и связанных объектов") } } diff --git a/backend/src/services/wireguard-import-apply.test.ts b/backend/src/services/wireguard-import-apply.test.ts index 03c012a1..2a8baf06 100644 --- a/backend/src/services/wireguard-import-apply.test.ts +++ b/backend/src/services/wireguard-import-apply.test.ts @@ -13,6 +13,7 @@ function fixture() { const rows: Record = { [iface]: [{ ".id": "*F", name: "foreign-wg" }], [addresses]: [{ ".id": "*A", interface: "foreign-wg", address: "192.0.2.1/24" }], + "/ipv6/address": [], [peers]: [{ ".id": "*P", interface: "foreign-wg", "public-key": "foreign-peer" }], } const baseline = structuredClone(rows), deletes: string[] = [] @@ -35,6 +36,38 @@ function fixture() { } const state = (expected: WireguardImportApplyError["state"]) => (error: unknown) => error instanceof WireguardImportApplyError && error.state === expected +test("IPv6 import confirms normalized receipt, ignores automatic link-local, preserves IPv4 with identical ID", async () => { + const f = fixture(), put = f.client.put + f.rows[addresses].push({ ".id": "*OWN/2", interface: "foreign-wg", address: "192.0.2.8/24" }) + f.client.put = async (path, value) => { + const result = await put(path, value) + if (path === "/ipv6/address") f.rows[path].at(-1)!.address = "fd00:152::1/64" + if (path === iface) f.rows["/ipv6/address"].push({ ".id": "*D", interface: "own-wg", address: "fe80::1/64", dynamic: "true" }) + return result + } + await applyWireguardImport(f.client, { ...parsed, interface: { ...parsed.interface, address: "fd00:0152:0:0:0:0:0:1/64" } }, body) + assert(f.rows["/ipv6/address"].some(row => row.address === "fd00:152::1/64" && row.advertise === "no")) + assert(f.rows[addresses].some(row => row[".id"] === "*OWN/2" && row.interface === "foreign-wg")) + assert.deepEqual(f.deletes, []) +}) + +test("IPv6 read failure prevents every import mutation", async () => { + const f = fixture(), get = f.client.get + f.client.get = async path => { if (path === "/ipv6/address") throw new Error("IPv6 denied"); return get(path) } + await assert.rejects(applyWireguardImport(f.client, parsed, body), /IPv6 denied/) + assert.equal(f.puts(), 0) +}) + +test("rejected peer compensates only owned IPv6 address, not colliding IPv4 ID", async () => { + const f = fixture(), put = f.client.put + f.rows[addresses].push({ ".id": "*OWN/2", interface: "foreign-wg", address: "192.0.2.8/24" }) + f.client.put = async (path, value) => { if (path === peers) throw new MikrotikError(400, path, "rejected"); return put(path, value) } + await assert.rejects(applyWireguardImport(f.client, { ...parsed, interface: { ...parsed.interface, address: "fd00:152::1/64" } }, body), state("rolled-back")) + assert(f.deletes.includes("/ipv6/address/*OWN%2F2")) + assert(!f.deletes.some(path => path.startsWith(addresses))) + assert(f.rows[addresses].some(row => row[".id"] === "*OWN/2")) +}) + test("baseline read failure prevents every mutation", async () => { const f = fixture() f.client.get = async () => { throw new Error("Read unavailable") } diff --git a/backend/src/services/wireguard-import-apply.ts b/backend/src/services/wireguard-import-apply.ts index 476b6057..c274e722 100644 --- a/backend/src/services/wireguard-import-apply.ts +++ b/backend/src/services/wireguard-import-apply.ts @@ -1,10 +1,11 @@ import { MikrotikError, type MikrotikClient } from "./mikrotik.js" import type { WgParsedConfig, WgParsedPeer } from "./wireguard-config.js" import { asRosArray, toRosBody } from "./wireguard-ros.js" +import { interfaceAddressPath, interfaceAddressKey } from "./entity-snapshots.js" type Client = Pick type Row = Record -const paths = ["/interface/wireguard", "/ip/address", "/interface/wireguard/peers"] as const +const paths = ["/interface/wireguard", "/ip/address", "/interface/wireguard/peers", "/ipv6/address"] as const type Created = { path: typeof paths[number]; id: string; identity: Record } export class WireguardImportApplyError extends Error { @@ -24,12 +25,17 @@ export async function applyWireguardImport( peerBody: (peer: WgParsedPeer, interfaceName: string) => Record, ): Promise<{ interfaceName: string; peersCreated: number }> { const name = parsed.interface.name - const read = async () => Object.fromEntries(await Promise.all(paths.map(async path => [path, asRosArray(await client.get(path))]))) as Record + const addressPath = parsed.interface.address ? interfaceAddressPath(parsed.interface.address) : undefined + const read = async () => Object.fromEntries(await Promise.all(paths.map(async path => { + const rows = await client.get(path) + if (!Array.isArray(rows)) throw new Error("Неполный каталог WireGuard для подтверждения импорта") + return [path, (rows as Row[]).filter(row => !path.endsWith("/address") || (row.dynamic !== "true" && row.dynamic !== "yes"))] + }))) as Record const baseline = await read() if (baseline[paths[0]].some(row => row.name === name)) throw new WireguardImportApplyError("conflict") const created: Created[] = [] let unconfirmed = false - const owns = (row: Row | undefined, item: Created) => !!row && Object.entries(item.identity).every(([key, value]) => row[key] === value) + const owns = (row: Row | undefined, item: Created) => !!row && Object.entries(item.identity).every(([key, value]) => key === "address" ? !!row.address && interfaceAddressKey(row.address) === interfaceAddressKey(value) : row[key] === value) const put = async (path: typeof paths[number], body: Record, identity: Record) => { let receipt: unknown try { @@ -49,8 +55,9 @@ export async function applyWireguardImport( } try { await put(paths[0], toRosBody({ name, "listen-port": String(parsed.interface.listenPort ?? 13231), mtu: String(parsed.interface.mtu ?? 1420), "private-key": parsed.interface.privateKey, comment: parsed.interface.comment, disabled: parsed.interface.disabled ? "yes" : undefined }), { name }) - if (parsed.interface.address) { - await put(paths[1], { address: parsed.interface.address, interface: name }, { address: parsed.interface.address, interface: name }) + if (parsed.interface.address && addressPath) { + const path = addressPath + await put(path, { address: parsed.interface.address, interface: name, ...(path === "/ipv6/address" ? { advertise: "no" } : {}) }, { address: parsed.interface.address, interface: name }) } for (const peer of parsed.peers) { await put(paths[2], peerBody(peer, name), { interface: name, "public-key": peer.publicKey }) diff --git a/backend/src/services/wireguard-ipv6.test.ts b/backend/src/services/wireguard-ipv6.test.ts new file mode 100644 index 00000000..8aeb6f11 --- /dev/null +++ b/backend/src/services/wireguard-ipv6.test.ts @@ -0,0 +1,52 @@ +import assert from "node:assert/strict" +import { MikrotikClient } from "./mikrotik.js" +import { fetchWireguardRestoreState } from "./wireguard-live.js" +import { putIpAddress } from "./wireguard-ros.js" +import { canonicalWireguardSnapshot, planWireguardRestore, InterfaceAddressConflict } from "./entity-snapshots.js" +import { parseWireguardImport } from "./wireguard-import.js" +import { generateMikrotikRsc } from "./wireguard-config.js" +import { wgCreateInterfaceSchema, wgPatchInterfaceSchema } from "@mmapp/contracts/wireguard" + +const original = MikrotikClient.fromServer +const calls: string[] = [] +const raw: Record = { + "/interface/wireguard": [{ ".id": "*w", name: "wg-own", "private-key": "" }], + "/interface/wireguard/peers": [], + "/ip/address": [{ ".id": "*1", interface: "wg-own", address: "10.1.0.1/30" }], + "/ipv6/address": [{ ".id": "*1", interface: "wg-own", address: "fd00:152::1/64", advertise: "true", comment: "own", "no-dad": "true" }, { ".id": "*2", interface: "wg-own", address: "fd00:152::2/64", disabled: "true" }, { ".id": "*3", interface: "wg-own", address: "fe80::1/64", dynamic: "true" }], +} +try { + MikrotikClient.fromServer = () => ({ get: async (path: string) => { calls.push(path); return structuredClone(raw[path]) } }) as unknown as MikrotikClient + const state = await fetchWireguardRestoreState({ id: 1 } as Parameters[0]) + assert.equal(state.addrs.length, 3) + assert.equal(state.snapshot.interfaces[0]?.innerAddresses?.length, 3) + assert.equal(planWireguardRestore(state.snapshot, state).filter(op => op.path.includes("/address")).length, 0) + const legacy = canonicalWireguardSnapshot({ interfaces: [{ name: "wg-own", address: "fd00:152::1/64" }] }) + assert.throws(() => planWireguardRestore(legacy, state), InterfaceAddressConflict) + const desired = structuredClone(state.snapshot) + desired.interfaces[0]!.innerAddresses = [{ address: "fd00:152::9/64", disabled: false }] + const ops = planWireguardRestore(desired, state) + assert(ops.some(op => op.op === "delete" && op.path === "/ip/address/*1")) + assert(ops.some(op => op.op === "delete" && op.path === "/ipv6/address/*1")) + assert(ops.some(op => op.op === "delete" && op.path === "/ipv6/address/*2")) + assert(!ops.some(op => op.path.endsWith("/*3"))) + assert(ops.some(op => op.op === "put" && op.path === "/ipv6/address" && op.body.advertise === "no")) + await putIpAddress({ put: async (path: string, body: Record) => { assert.equal(path, "/ipv6/address"); assert.equal(body.advertise, "no") } } as unknown as MikrotikClient, "fd00::1/64", "wg-own") + MikrotikClient.fromServer = () => ({ get: async (path: string) => { if (path === "/ipv6/address") throw new Error("Unauthorized"); return [] } }) as unknown as MikrotikClient + await assert.rejects(fetchWireguardRestoreState({ id: 1 } as Parameters[0]), /Unauthorized/) +} finally { MikrotikClient.fromServer = original } +const key = Buffer.alloc(32, 7).toString("base64") +const rsc = generateMikrotikRsc({ name: "wg-own", mtu: 1400, address: "fd00:152::1/64", privateKey: key, peers: [] }) +assert(rsc.includes("/ipv6 address add")) +assert(rsc.includes("advertise=no")) +assert.equal(parseWireguardImport(rsc, "rsc").interface.address, "fd00:152::1/64") +assert.throws(() => parseWireguardImport(rsc.replace("interface=wg-own", "interface=foreign"), "rsc"), /другому интерфейсу/) +assert.throws(() => parseWireguardImport(rsc + "\n/ipv6 address add address=fd00::2/64 interface=wg-own", "rsc"), /нескольких/) +assert.throws(() => parseWireguardImport(rsc.replace("advertise=no", "advertise=yes"), "rsc"), /специальных/) +for (const address of ["fd00::1", "fd00::1/129", "bad/64", "10.0.0.1/33"]) { + assert(!wgCreateInterfaceSchema.safeParse({ serverId: 1, name: "own", address }).success) + assert(!wgPatchInterfaceSchema.safeParse({ address }).success) +} +assert(wgCreateInterfaceSchema.safeParse({ serverId: 1, name: "own", address: "fd00::1/64" }).success) +assert(wgPatchInterfaceSchema.safeParse({ address: "" }).success) +console.log("WireGuard IPv6: dual-stack static snapshot, family restore, legacy conflict, failed read, native IPv6 PUT, RSC roundtrip and contracts passed") diff --git a/backend/src/services/wireguard-live.ts b/backend/src/services/wireguard-live.ts index 42d5b06c..c8624c89 100644 --- a/backend/src/services/wireguard-live.ts +++ b/backend/src/services/wireguard-live.ts @@ -1,8 +1,9 @@ import { parseWireGuardBytes as parseBytes } from "./wireguard-observed-values.js" import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js" -import { eq } from "drizzle-orm" +import { and, eq } from "drizzle-orm" import { db } from "../db/index.js" import { servers } from "../db/schema.js" +import { parseDbServerId } from "../utils/server-id.js" import { MikrotikClient } from "./mikrotik.js" import type { WgIfaceDto, WgPeerDto } from "@mmapp/contracts/wireguard" import { @@ -52,6 +53,12 @@ interface RosIpAddress { address?: string interface?: string disabled?: string + dynamic?: string + advertise?: string + comment?: string + "no-dad"?: string + "from-pool"?: string + "eui-64"?: string } function mapPeer(p: RosWireGuardPeer, idx: number): WgPeerDto { @@ -121,11 +128,14 @@ async function fetchForServer( includePrivateKey: boolean, ): Promise { const client = MikrotikClient.fromServer(server) - const [ifacesRaw, peersRaw, addrsRaw] = await Promise.all([ + const [ifacesRaw, peersRaw, v4Raw, v6Raw] = await Promise.all([ client.get("/interface/wireguard"), client.get("/interface/wireguard/peers"), - client.get("/ip/address").catch(() => [] as RosIpAddress[]), + client.get("/ip/address"), + client.get("/ipv6/address"), ]) + if (![ifacesRaw, peersRaw, v4Raw, v6Raw].every(Array.isArray)) throw new Error("RouterOS: неполный каталог WireGuard/адресов") + const addrsRaw = [...v4Raw, ...v6Raw].sort((a, b) => (a.address ?? "").localeCompare(b.address ?? "")) const peersByIface = new Map() peersRaw.forEach((p, idx) => { @@ -138,7 +148,7 @@ async function fetchForServer( const addrByIface = new Map() for (const a of addrsRaw) { - if (a.disabled === "true" || a.disabled === "yes") continue + if (a.disabled === "true" || a.disabled === "yes" || a.dynamic === "true" || a.dynamic === "yes") continue const iface = (a.interface ?? "").trim() const addr = (a.address ?? "").trim() if (iface && addr && !addrByIface.has(iface)) addrByIface.set(iface, addr) @@ -164,11 +174,14 @@ export async function fetchWireguardRestoreState(server: ServerRow): Promise<{ snapshot: WgSnapshot }> { const client = MikrotikClient.fromServer(server) - const [ifacesRaw, peersRaw, addrsRaw] = await Promise.all([ + const [ifacesRaw, peersRaw, v4Raw, v6Raw] = await Promise.all([ client.get("/interface/wireguard"), client.get("/interface/wireguard/peers"), - client.get("/ip/address").catch(() => [] as RosIpAddress[]), + client.get("/ip/address"), + client.get("/ipv6/address"), ]) + if (![ifacesRaw, peersRaw, v4Raw, v6Raw].every(Array.isArray)) throw new Error("RouterOS: неполный каталог WireGuard/адресов") + const addrsRaw = [...v4Raw, ...v6Raw].sort((a, b) => Number(a.disabled === "true" || a.disabled === "yes") - Number(b.disabled === "true" || b.disabled === "yes") || (a.address ?? "").localeCompare(b.address ?? "")) const ifaces: WgLiveIface[] = (Array.isArray(ifacesRaw) ? ifacesRaw : []).map((w) => ({ name: (w.name ?? "").trim(), @@ -205,13 +218,15 @@ export async function fetchWireguardRestoreState(server: ServerRow): Promise<{ const addrs: WgLiveAddr[] = [] for (const a of Array.isArray(addrsRaw) ? addrsRaw : []) { - if (a.disabled === "true" || a.disabled === "yes") continue + if (a.dynamic === "true" || a.dynamic === "yes") continue const iface = (a.interface ?? "").trim() const address = (a.address ?? "").trim() const rosId = String(a[".id"] ?? "") if (!iface || !address || !rosId) continue if (!ifaces.some((i) => i.name === iface)) continue - addrs.push({ rosId, interfaceName: iface, address }) + addrs.push({ rosId, interfaceName: iface, address, disabled: a.disabled === "true" || a.disabled === "yes", comment: a.comment ?? "", + ...(address.includes(":") ? { advertise: a.advertise === "true" || a.advertise === "yes", noDad: a["no-dad"] === "true" || a["no-dad"] === "yes", fromPool: a["from-pool"] ?? "", eui64: a["eui-64"] === "true" || a["eui-64"] === "yes" } : {}), + }) } const snapshot = canonicalWireguardSnapshot({ @@ -221,6 +236,7 @@ export async function fetchWireguardRestoreState(server: ServerRow): Promise<{ mtu: iface.mtu, privateKey: iface.privateKey, address: addrs.find((a) => a.interfaceName === iface.name)?.address ?? "", + innerAddresses: addrs.filter(a => a.interfaceName === iface.name).map(a => ({ address: a.address, disabled: Boolean(a.disabled), comment: a.comment, ...(a.advertise === undefined ? {} : { advertise: a.advertise, noDad: a.noDad, fromPool: a.fromPool, eui64: a.eui64 }) })), comment: iface.comment, disabled: iface.disabled, peers: peers.filter((p) => p.interfaceName === iface.name), @@ -247,11 +263,11 @@ export async function listWireGuardInterfaces(opts?: { const includePrivateKey = opts?.includePrivateKey === true let serverRows: ServerRow[] if (opts?.serverId) { - const id = Number.parseInt(String(opts.serverId), 10) - if (!Number.isFinite(id)) { + const id = parseDbServerId(opts.serverId) + if (id === null || !Number.isSafeInteger(id) || id > 2147483647) { return { interfaces: [], failures: [{ serverId: String(opts.serverId), error: "Некорректный serverId" }] } } - const row = (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0] + const row = (await db.select().from(servers).where(and(eq(servers.id, id), eq(servers.enabled, true))).limit(1))[0] serverRows = row ? [row] : [] } else { serverRows = await db.select().from(servers).where(eq(servers.enabled, true)) @@ -278,9 +294,9 @@ export async function listWireGuardInterfaces(opts?: { export async function countWireGuardInterfaces():Promise{return boundedCompleteCount(()=>listWireGuardInterfaces({ includePrivateKey: false }),value=>completeCollectionCount(value,"interfaces"))} export async function getEnabledServerById(serverId: string | number) { - const id = typeof serverId === "number" ? serverId : Number.parseInt(String(serverId), 10) - if (!Number.isFinite(id)) return null - return (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0] ?? null + const id = parseDbServerId(serverId) + if (id === null || !Number.isSafeInteger(id) || id > 2147483647) return null + return (await db.select().from(servers).where(and(eq(servers.id, id), eq(servers.enabled, true))).limit(1))[0] ?? null } export type CatalogWgPeer = { diff --git a/backend/src/services/wireguard-ros.ts b/backend/src/services/wireguard-ros.ts index c0bc509a..bcbcbb8b 100644 --- a/backend/src/services/wireguard-ros.ts +++ b/backend/src/services/wireguard-ros.ts @@ -1,4 +1,5 @@ import type { MikrotikClient } from "./mikrotik.js" +import { interfaceAddressPath } from "./entity-snapshots.js" /** Общие PUT iface / peer / address для `/wireguard` и traffic-flow overlay. */ export function toRosBody(obj: Record): Record { @@ -31,7 +32,8 @@ export async function putIpAddress( address: string, iface: string, ): Promise { - await client.put("/ip/address", { address, interface: iface }) + const path = interfaceAddressPath(address) + await client.put(path, { address, interface: iface, ...(path === "/ipv6/address" ? { advertise: "no" } : {}) }) } export async function putWireguardPeer( diff --git a/components/wireguard/wg-create-sheet.tsx b/components/wireguard/wg-create-sheet.tsx index 7e934e4e..01d15c58 100644 --- a/components/wireguard/wg-create-sheet.tsx +++ b/components/wireguard/wg-create-sheet.tsx @@ -157,10 +157,10 @@ function WgCreateSheet({ {form.showAdvanced && (
- + set("address", e.target.value)} /> diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index 50e6b538..5eda356e 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -1,6 +1,6 @@ # UX/UI: протокол внешней приёмки 12.5 -Актуальное продолжение 2026-10-11: завершён12.5.151 — GRE IPv6 static-address lifecycle через приложение: чтение/create/PATCH/history restore/delete, дополнительные/выключенные адреса, family-safe IDs и legacy409.15 профильных E2E, source types/ESLint, обе production сборки, 984 compiled parity и real native API/SQL/Ping3/3×2/cleanup прошли. Runtime backend/frontend ux-gre151 healthy;12.5 открыт. Следующий152 — application WireGuard IPv6 address lifecycle, включая импорт/историю/удаление; работать укрупнённым разделом без повторения принятого GRE. +Актуальное продолжение 2026-10-11: завершён12.5.152 — WireGuard IPv6 static lifecycle/create/PATCH/history/delete и single-address RSC/conf export/import.29 real API cases, native handshake/Ping3/3×2,33 disabled-server API/SQL cases всех трёх типов с zero ROS contacts,56 backend tests/профили GRE и контейнеров/live maps,10 E2E/types/ESLint/builds/984 parity/live PC/mobile passed. Runtime backend/frontend ux-wg152 healthy;12.5 открыт. Следующий153 — OSPF/BFD sustained gate: BFD-only loss при сохранённых OSPF Hello на собственных GRE, восстановление и independent cleanup. 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). @@ -696,7 +696,7 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-11: завершён12.5.151 — GRE IPv6 static-address lifecycle через приложение: чтение/create/PATCH/history restore/delete, дополнительные/выключенные адреса, family-safe IDs и legacy409.15 профильных E2E, source types/ESLint, обе production сборки, 984 compiled parity и real native API/SQL/Ping3/3×2/cleanup прошли. Runtime backend/frontend ux-gre151 healthy;12.5 открыт. Следующий152 — application WireGuard IPv6 address lifecycle, включая импорт/историю/удаление; работать укрупнённым разделом без повторения принятого GRE. +Актуальное продолжение 2026-10-11: завершён12.5.152 — WireGuard IPv6 static lifecycle/create/PATCH/history/delete и single-address RSC/conf export/import.29 real API cases, native handshake/Ping3/3×2,33 disabled-server API/SQL cases всех трёх типов с zero ROS contacts,56 backend tests/профили GRE и контейнеров/live maps,10 E2E/types/ESLint/builds/984 parity/live PC/mobile passed. Runtime backend/frontend ux-wg152 healthy;12.5 открыт. Следующий153 — OSPF/BFD sustained gate: BFD-only loss при сохранённых OSPF Hello на собственных GRE, восстановление и independent cleanup. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). @@ -1649,3 +1649,23 @@ Create/PATCH/restore/delete выбирают /ip/address или /ipv6/address п Протоколы: [native/cleanup](ux-ui/acceptance/gre-ipv6-native-2026-10-11.json), [API/history](ux-ui/acceptance/gre-ipv6-api-history-2026-10-11.json), [первый запуск](ux-ui/acceptance/gre-ipv6-initial-cleanup-2026-10-11.json), [compiled parity](ux-ui/acceptance/gre-ipv6-compiled-2026-10-11.json), [live UI](ux-ui/acceptance/gre-ipv6-smoke-2026-10-11.json). Границы: IPv6 ULA inner по IPv4 GRE outer; IPv6 outer/public WAN, scale, длительное восстановление и from-pool/EUI-64 не приняты. Полная регрессия после139 не повторялась: выполнен профиль изменённых областей. Следующий152 — WireGuard IPv6 lifecycle;12.5 остаётся открытым. + +## 12.5.152 — IPv6 lifecycle WireGuard и общий допуск выключенных серверов (2026-10-11) + +WireGuard читает оба address catalogs; ошибки/не-массивы не превращаются в пустые данные. Catalog показывает активный статический адрес, новые snapshots сохраняют все статические адреса с disabled/advertise/comment/no-dad. Dynamic link-local исключён из управления. Create и PATCH address, history restore и delete выбирают семейство по CIDR; дополнительные адреса сохранены, legacy snapshot с несколькими текущими адресами409. Общий адресный planner переиспользован GRE/WireGuard; constructor alias GreRestoreConflict совместим. PATCH не переименовывает одновременно интерфейс и адрес. + +Import подтверждает receipts в /ip/address и /ipv6/address раздельно, normalized IPv6 identities принимаются, automatic link-local не считается чужим static child. Lost/unconfirmed receipt не разрешает удаление по одному имени; rollback не удаляет IPv4 с совпавшим IPv6 ID. IPv6 RSC export использует /ipv6 address add +advertise=no, parser распознаёт IPv6 address line. Multiple/special address import и export явно отклоняются вместо тихой потери данных. Это single-address literal import/export, не произвольный RouterOS configuration codec. Существующий ReUI create sheet получил подсказку IPv4/IPv6; структура/Frame/base-nova сохранены. + +При native приёмке обнаружено: getEnabledServerById ранее проверял только ID. Теперь общий getter допускает только enabled=true и полный корректный DB ID; WireGuard selected catalog также исключает disabled. Это влияет на использующих getter GRE/WireGuard/firewall/containers/traffic/overlay. Постоянный opt-in test network-disabled-server.test.ts делает33 API/SQL cases: три типа Home Router/Jump Host/Exit Node ×11 calls, factory любого ROS client запрещена, contacts=0, enabled positive control проходит.8 malformed IDs проверяются до SQL, включая unsafe/выход за integer DB range. Без MMAPP_OWN_TEST_DB SQL часть честно skipped; в собственном PG стенде выполнена и база удалена. + +Проверки:56 node:test cases import/rollback/revision/delete, service/config/HTTP scripts, GRE config-sync и14 invalid GRE requests, containers/live-maps profiles;10 E2E (WireGuard IPv6/error-retained input PC/mobile, tables/menus и GRE smoke после общего planner), source types/ESLint; обе production сборки,984 emitted outputs совпали с окончательным backend. + +Native изолированная PG + production handlers + оба CHR:29 API cases, native handshake0s и Ping3/3 в обе стороны после IPv6 invalid=false; create/PATCH/IPv6→IPv4/clear, multiple-address history restore, legacy409, export multiple409, RSC/conf single IPv6 export и реальный import/delete, disabled GET empty/PATCH404 с forbidden factory. После каждого запуска9 static collections×2 совпали, собственные интерфейсы/peers/адреса и PG базы удалены. Native source/cleanup не меняли пароли/public/default routes. Background polling главного приложения продолжался; отсутствие всех background SQL writes не заявляется. + +Первый сценарий остановился из-за ошибки теста: ожидал200 от POST peer, фактический корректный201. После исправления сценарий прошёл весь native lifecycle, но завершающий disabled PATCH вернул500 из-за реального дефекта общего getter. Оба отрицательных протокола сохранены; getter исправлен,33 isolated API/SQL cases и полный native сценарий на финальном образе прошли. Frontend повторно не собирался из-за backend-only gate delta. + +Runtime backend/frontend ux-wg152 healthy; deployed live read-only catalog/create-sheet на1440/320 passed после cleanup. Browser mutations покрыты controlled E2E, реальные RouterOS mutations — production handlers в выделенной базе. IPv6 ULA inner по IPv4 WireGuard outer, не IPv6 public WAN/client OS/throughput/scale/lost ACK physical acceptance. From-pool/EUI-64/multiple-address import/export остаются явными ограничениями; full regression после139 не повторялась. + +Протоколы: [native/cleanup](ux-ui/acceptance/wireguard-ipv6-native-2026-10-11.json), [API/history/import/export](ux-ui/acceptance/wireguard-ipv6-api-history-2026-10-11.json), [33 SQL/API gates](ux-ui/acceptance/network-disabled-sql-2026-10-11.json), [до исправления getter](ux-ui/acceptance/wireguard-ipv6-before-gate-api-2026-10-11.json), [compiled](ux-ui/acceptance/wireguard-ipv6-compiled-2026-10-11.json), [live UI](ux-ui/acceptance/wireguard-ipv6-smoke-2026-10-11.json). + +Следующий153 — sustained OSPF/BFD-only gate и recovery на собственных GRE;12.5 остаётся открытым. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 5b32bfb1..4465ef4f 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,12 +1,12 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-11: завершён12.5.151 — GRE IPv6 static-address lifecycle через приложение: чтение/create/PATCH/history restore/delete, дополнительные/выключенные адреса, family-safe IDs и legacy409.15 профильных E2E, source types/ESLint, обе production сборки, 984 compiled parity и real native API/SQL/Ping3/3×2/cleanup прошли. Runtime backend/frontend ux-gre151 healthy;12.5 открыт. Следующий152 — application WireGuard IPv6 address lifecycle, включая импорт/историю/удаление; работать укрупнённым разделом без повторения принятого GRE. +Актуальное продолжение 2026-10-11: завершён12.5.152 — WireGuard IPv6 static lifecycle/create/PATCH/history/delete и single-address RSC/conf export/import.29 real API cases, native handshake/Ping3/3×2,33 disabled-server API/SQL cases всех трёх типов с zero ROS contacts,56 backend tests/профили GRE и контейнеров/live maps,10 E2E/types/ESLint/builds/984 parity/live PC/mobile passed. Runtime backend/frontend ux-wg152 healthy;12.5 открыт. Следующий153 — OSPF/BFD sustained gate: BFD-only loss при сохранённых OSPF Hello на собственных GRE, восстановление и independent cleanup. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-10. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. ## Актуальная точка продолжения — 2026-10-10 -Актуальное продолжение 2026-10-11: завершён12.5.151 — GRE IPv6 static-address lifecycle через приложение: чтение/create/PATCH/history restore/delete, дополнительные/выключенные адреса, family-safe IDs и legacy409.15 профильных E2E, source types/ESLint, обе production сборки, 984 compiled parity и real native API/SQL/Ping3/3×2/cleanup прошли. Runtime backend/frontend ux-gre151 healthy;12.5 открыт. Следующий152 — application WireGuard IPv6 address lifecycle, включая импорт/историю/удаление; работать укрупнённым разделом без повторения принятого GRE. +Актуальное продолжение 2026-10-11: завершён12.5.152 — WireGuard IPv6 static lifecycle/create/PATCH/history/delete и single-address RSC/conf export/import.29 real API cases, native handshake/Ping3/3×2,33 disabled-server API/SQL cases всех трёх типов с zero ROS contacts,56 backend tests/профили GRE и контейнеров/live maps,10 E2E/types/ESLint/builds/984 parity/live PC/mobile passed. Runtime backend/frontend ux-wg152 healthy;12.5 открыт. Следующий153 — OSPF/BFD sustained gate: BFD-only loss при сохранённых OSPF Hello на собственных GRE, восстановление и independent cleanup. Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты. @@ -371,6 +371,6 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-11: завершён12.5.151 — GRE IPv6 static-address lifecycle через приложение: чтение/create/PATCH/history restore/delete, дополнительные/выключенные адреса, family-safe IDs и legacy409.15 профильных E2E, source types/ESLint, обе production сборки, 984 compiled parity и real native API/SQL/Ping3/3×2/cleanup прошли. Runtime backend/frontend ux-gre151 healthy;12.5 открыт. Следующий152 — application WireGuard IPv6 address lifecycle, включая импорт/историю/удаление; работать укрупнённым разделом без повторения принятого GRE. +Актуальное продолжение 2026-10-11: завершён12.5.152 — WireGuard IPv6 static lifecycle/create/PATCH/history/delete и single-address RSC/conf export/import.29 real API cases, native handshake/Ping3/3×2,33 disabled-server API/SQL cases всех трёх типов с zero ROS contacts,56 backend tests/профили GRE и контейнеров/live maps,10 E2E/types/ESLint/builds/984 parity/live PC/mobile passed. Runtime backend/frontend ux-wg152 healthy;12.5 открыт. Следующий153 — OSPF/BFD sustained gate: BFD-only loss при сохранённых OSPF Hello на собственных GRE, восстановление и independent cleanup. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 7541419d..e7a5125d 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-11: завершён12.5.151 — GRE IPv6 static-address lifecycle через приложение: чтение/create/PATCH/history restore/delete, дополнительные/выключенные адреса, family-safe IDs и legacy409.15 профильных E2E, source types/ESLint, обе production сборки, 984 compiled parity и real native API/SQL/Ping3/3×2/cleanup прошли. Runtime backend/frontend ux-gre151 healthy;12.5 открыт. Следующий152 — application WireGuard IPv6 address lifecycle, включая импорт/историю/удаление; работать укрупнённым разделом без повторения принятого GRE. +Актуальное продолжение 2026-10-11: завершён12.5.152 — WireGuard IPv6 static lifecycle/create/PATCH/history/delete и single-address RSC/conf export/import.29 real API cases, native handshake/Ping3/3×2,33 disabled-server API/SQL cases всех трёх типов с zero ROS contacts,56 backend tests/профили GRE и контейнеров/live maps,10 E2E/types/ESLint/builds/984 parity/live PC/mobile passed. Runtime backend/frontend ux-wg152 healthy;12.5 открыт. Следующий153 — OSPF/BFD sustained gate: BFD-only loss при сохранённых OSPF Hello на собственных GRE, восстановление и independent cleanup. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -36,7 +36,27 @@ | 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–151 и ограничения ниже. Runtime backend ux-month149/frontend ux-sessions150. | +| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–152 и ограничения ниже. Runtime backend ux-month149/frontend ux-sessions150. | + +## 12.5.152 — IPv6 lifecycle WireGuard и общий допуск выключенных серверов (2026-10-11) + +WireGuard читает оба address catalogs; ошибки/не-массивы не превращаются в пустые данные. Catalog показывает активный статический адрес, новые snapshots сохраняют все статические адреса с disabled/advertise/comment/no-dad. Dynamic link-local исключён из управления. Create и PATCH address, history restore и delete выбирают семейство по CIDR; дополнительные адреса сохранены, legacy snapshot с несколькими текущими адресами409. Общий адресный planner переиспользован GRE/WireGuard; constructor alias GreRestoreConflict совместим. PATCH не переименовывает одновременно интерфейс и адрес. + +Import подтверждает receipts в /ip/address и /ipv6/address раздельно, normalized IPv6 identities принимаются, automatic link-local не считается чужим static child. Lost/unconfirmed receipt не разрешает удаление по одному имени; rollback не удаляет IPv4 с совпавшим IPv6 ID. IPv6 RSC export использует /ipv6 address add +advertise=no, parser распознаёт IPv6 address line. Multiple/special address import и export явно отклоняются вместо тихой потери данных. Это single-address literal import/export, не произвольный RouterOS configuration codec. Существующий ReUI create sheet получил подсказку IPv4/IPv6; структура/Frame/base-nova сохранены. + +При native приёмке обнаружено: getEnabledServerById ранее проверял только ID. Теперь общий getter допускает только enabled=true и полный корректный DB ID; WireGuard selected catalog также исключает disabled. Это влияет на использующих getter GRE/WireGuard/firewall/containers/traffic/overlay. Постоянный opt-in test network-disabled-server.test.ts делает33 API/SQL cases: три типа Home Router/Jump Host/Exit Node ×11 calls, factory любого ROS client запрещена, contacts=0, enabled positive control проходит.8 malformed IDs проверяются до SQL, включая unsafe/выход за integer DB range. Без MMAPP_OWN_TEST_DB SQL часть честно skipped; в собственном PG стенде выполнена и база удалена. + +Проверки:56 node:test cases import/rollback/revision/delete, service/config/HTTP scripts, GRE config-sync и14 invalid GRE requests, containers/live-maps profiles;10 E2E (WireGuard IPv6/error-retained input PC/mobile, tables/menus и GRE smoke после общего planner), source types/ESLint; обе production сборки,984 emitted outputs совпали с окончательным backend. + +Native изолированная PG + production handlers + оба CHR:29 API cases, native handshake0s и Ping3/3 в обе стороны после IPv6 invalid=false; create/PATCH/IPv6→IPv4/clear, multiple-address history restore, legacy409, export multiple409, RSC/conf single IPv6 export и реальный import/delete, disabled GET empty/PATCH404 с forbidden factory. После каждого запуска9 static collections×2 совпали, собственные интерфейсы/peers/адреса и PG базы удалены. Native source/cleanup не меняли пароли/public/default routes. Background polling главного приложения продолжался; отсутствие всех background SQL writes не заявляется. + +Первый сценарий остановился из-за ошибки теста: ожидал200 от POST peer, фактический корректный201. После исправления сценарий прошёл весь native lifecycle, но завершающий disabled PATCH вернул500 из-за реального дефекта общего getter. Оба отрицательных протокола сохранены; getter исправлен,33 isolated API/SQL cases и полный native сценарий на финальном образе прошли. Frontend повторно не собирался из-за backend-only gate delta. + +Runtime backend/frontend ux-wg152 healthy; deployed live read-only catalog/create-sheet на1440/320 passed после cleanup. Browser mutations покрыты controlled E2E, реальные RouterOS mutations — production handlers в выделенной базе. IPv6 ULA inner по IPv4 WireGuard outer, не IPv6 public WAN/client OS/throughput/scale/lost ACK physical acceptance. From-pool/EUI-64/multiple-address import/export остаются явными ограничениями; full regression после139 не повторялась. + +Протоколы: [native/cleanup](ux-ui/acceptance/wireguard-ipv6-native-2026-10-11.json), [API/history/import/export](ux-ui/acceptance/wireguard-ipv6-api-history-2026-10-11.json), [33 SQL/API gates](ux-ui/acceptance/network-disabled-sql-2026-10-11.json), [до исправления getter](ux-ui/acceptance/wireguard-ipv6-before-gate-api-2026-10-11.json), [compiled](ux-ui/acceptance/wireguard-ipv6-compiled-2026-10-11.json), [live UI](ux-ui/acceptance/wireguard-ipv6-smoke-2026-10-11.json). + +Следующий153 — sustained OSPF/BFD-only gate и recovery на собственных GRE;12.5 остаётся открытым. ## 12.5.151 — управление статическими IPv6-адресами GRE (2026-10-11) diff --git a/docs/ux-ui-remaining-acceptance.md b/docs/ux-ui-remaining-acceptance.md index debc3bb1..c0cf59f1 100644 --- a/docs/ux-ui-remaining-acceptance.md +++ b/docs/ux-ui-remaining-acceptance.md @@ -1,8 +1,8 @@ # Оставшаяся внешняя приёмка — 2026-10-11 -Это актуальная матрица после 12.5.151. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. +Это актуальная матрица после 12.5.152. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. -Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-gre151`, frontend `ux-gre151`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. +Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-wg152`, frontend `ux-wg152`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. | Раздел | Подтверждено | Осталось / условие завершения | | --- | --- | --- | @@ -10,7 +10,7 @@ | Speed/uptime | TCP transmit/receive/both32–33, короткий UDP34, auth failure25, transport timeout27, интерфейс after-ping28–29, snapshot/history race23–24 | Busy/abort/physical WAN disconnect; HTTP receipt socket disconnect принят97; независимое доказательство multi-WAN source/path; Home→JH/JH↔Exit с асимметричными каналами ; ROS-10 POST/SQL/GET и три часовых пояса приняты96. Короткие тесты не доказывают maximum throughput | | Outbox | Production PG claim/dedupe/settlement/retry/JWT/browser checkpoint/a11y35–49 | Настоящий Telegram API/чат, физический timeout после возможной отправки, несколько sender процессов. Нужны тестовый бот/чат и прямое разрешение на конкретное сообщение; подменённый ответ не доказывает доставку | | GRE | Native lifecycle, metadata, live формы/меню/URL/Ping53–57; GRE используется в OSPF/BGP/IPsec последующих блоков; native IPv6 ULA inner109; application static IPv6 lifecycle151 | IPv6 outer, from-pool/EUI-64, независимые WAN, масштаб и длительное восстановление; принятие GRE не означает приёмку всех сценариев автоматического расписания | -| WireGuard | Native handshake/data plane58, deletion60, live create/peer/disable/enable/delete62, mobile64, export/import67–74; native IPv6 ULA inner/allowedIps/handshake109 | Клиентские ОС и произвольные конфигурации, IPv6 outer/scale и application IPv6 interface address lifecycle, физический lost ACK и устойчивое восстановление; `.conf` с placeholders — шаблон, не принятый клиентский tunnel | +| WireGuard | Native handshake/data plane58, deletion60, live create/peer/disable/enable/delete62, mobile64, export/import67–74; native IPv6 ULA inner/allowedIps/handshake109; application static IPv6 lifecycle152 | Клиентские ОС и произвольные конфигурации, IPv6 outer/scale, from-pool/EUI-64 и multiple-address import/export, физический lost ACK и устойчивое восстановление; `.conf` с placeholders — шаблон, не принятый клиентский tunnel | | VXLAN | Собственная пара/private Ping/catalog/URL/export75–76; bridge/FDB/port loss/recovery107; IPv6 ULA inner по IPv4 outer108 | API приложения read-only: create/delete проверены native. IPv6 outer, масштаб/производительность, профильные mutation RBAC/concurrency отдельно не приняты | | IPsec | PSK и сертификатное шифрование77–82, active peer/online/mode-config, scoped NAT/group, partial/unknown/applied receipts, restore conflicts, JWT; full DN/специальные символы/online110; literal CN namespace guards и explicit FQDN111; certificate/PSK shared ID unknown/recovery112; multiple sessions controlled snapshots113; shared certificate rows/target guards114; preserve-certificate UI/native DELETE115; explicit deletion binding/PSK collision116; legacy export binding118; real duplicate DN sessions/Linux Ping/UI119; DN DER/ASCII tag и FQDN SAN native120; implicit unique DNS SAN/mixed IDs/API/UI121; короткий CHILD/IKE rekey/reconnect122; server pin и unbound same-CA certificate refusal123; own RSA Root/Intermediate/client chain/CRLs/IKE/reauth138; external revoked CRL new-handshake refusal/existing-session boundary137; certificate-bound malformed/signed expired CRL IKE refusal/recovery136; certificate-bound missing-CDP404/IKE refusal and recovery135; external HTTP CRL download/404/cache preservation134; isolated diagnostic buffers132; own ECDSA P-256/Ping/API export131 and final18×2 inventory; isolated same-key/CA time boundary with explicit AUTH_FAILED130; valid-renewal control timeout/clock/log observation129; original/renewed controls and time-invalid timeout observation128; CRL-issued cert/CA no-write delete guards127; own CRL-enabled valid/revoked control126; local revoked flag/CRL-disabled reconnect observation125; certificate reauth/CHILD absence boundary/UI124 | Реальные duplicate certificate-derived DN sessions приняты119; arbitrary custom PKI/OIDs/policies/path-length and missing/intermediate-revoked chain; native intermediate chain accepted138; ECDSA P-256 accepted131;128/129 timeout cause still unknown,130 time boundary accepted; periodic refresh; existing IKE survives short external-revocation observation137, automatic termination not guaranteed; direct CHR-to-CHR CRL HTTP failure cause134; reauth/CHILD loss принят124; длительный rekey/переходная потеря пакетов; прочие ID типы/ADDR4/manual DN/custom PKI; native edit identical matching rules rejected; клиентские Windows/macOS/iOS/Android, прочие DN форматы/custom OIDs/IKE ID namespaces, полная PKI и policy/profile/proposal restore, физический disconnect, durable retry/multi-instance, IPv6/maximum throughput | | OSPF/BFD | Real Full/own routes84–85, раздельные наблюдения BFD86, effective disabled/optimizer cost/partial/RBAC87, alternate path90 и CIDR91 | Sustained BFD-only gate не принят: в86 сосед мог вновь статьFull при доступныхHello. В90 переходный Ping18/40 с55% loss: бесшовность не принята. REST timing — наблюдаемая граница, не SLA; IPv6/scale/independent WAN открыты | @@ -18,7 +18,7 @@ | Recursive routes | Production PUT/change/own history restore88, disabled guards, partial/unknown/applied outcomes, live формы/контекст | Автоматический rollback/retry, durable receipts и несколько экземпляров отсутствуют; PUT не является idempotent noop. Полная произвольная legacy catalog миграция не принята | | Diagnostics/terminal | Allowlist/native reads, Ping/trace/route/MTU/DNS, actual avg-rtt, disabled guards, JWT/live PC/mobile89; real default memory tail/log limits/API/UI133 | DNS использует backend resolver; MTU — ограниченная оценка, не гарантированный максимум. Stop ожидания не доказывает остановку RouterOS команды; arbitrary CLI и physical disconnect не приняты | | Portal/интеграции | Локальные и isolated JWT guards; UI/API error states в границах журналов | Настоящий Portal issuer/login/отзыв прав; RIPE SDK two-IP/no-fetch passed140, native IPv6 ULA flow pipeline passed145 (IPv4 transport), public IPv6 WAN/local licensed MaxMind/S3/container/certificate jobs и Gitea delivery требуют отдельных тестовых endpoints/аккаунтов. Не отправлять сообщения и не публиковать релиз без соответствующего поручения | -| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar passed149; Sessions labels passed150; application GRE static IPv6 lifecycle151 passed; следующий WireGuard IPv6 lifecycle152; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию | +| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar passed149; Sessions labels passed150; application GRE static IPv6 lifecycle151 passed; WireGuard static IPv6 lifecycle152 и disabled SQL/API gates passed; следующий sustained OSPF/BFD153; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию | Доказательства и точные ограничения: [полная история этапа12](ux-ui-stage-12.md), [внешний протокол](ux-ui-external-acceptance.md), [BGP93](ux-ui/acceptance/bgp-failover-2026-10-10.json), [паспорт стенда](ux-ui-test-server.md). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение. @@ -564,3 +564,23 @@ Create/PATCH/restore/delete выбирают /ip/address или /ipv6/address п Протоколы: [native/cleanup](ux-ui/acceptance/gre-ipv6-native-2026-10-11.json), [API/history](ux-ui/acceptance/gre-ipv6-api-history-2026-10-11.json), [первый запуск](ux-ui/acceptance/gre-ipv6-initial-cleanup-2026-10-11.json), [compiled parity](ux-ui/acceptance/gre-ipv6-compiled-2026-10-11.json), [live UI](ux-ui/acceptance/gre-ipv6-smoke-2026-10-11.json). Границы: IPv6 ULA inner по IPv4 GRE outer; IPv6 outer/public WAN, scale, длительное восстановление и from-pool/EUI-64 не приняты. Полная регрессия после139 не повторялась: выполнен профиль изменённых областей. Следующий152 — WireGuard IPv6 lifecycle;12.5 остаётся открытым. + +## 12.5.152 — IPv6 lifecycle WireGuard и общий допуск выключенных серверов (2026-10-11) + +WireGuard читает оба address catalogs; ошибки/не-массивы не превращаются в пустые данные. Catalog показывает активный статический адрес, новые snapshots сохраняют все статические адреса с disabled/advertise/comment/no-dad. Dynamic link-local исключён из управления. Create и PATCH address, history restore и delete выбирают семейство по CIDR; дополнительные адреса сохранены, legacy snapshot с несколькими текущими адресами409. Общий адресный planner переиспользован GRE/WireGuard; constructor alias GreRestoreConflict совместим. PATCH не переименовывает одновременно интерфейс и адрес. + +Import подтверждает receipts в /ip/address и /ipv6/address раздельно, normalized IPv6 identities принимаются, automatic link-local не считается чужим static child. Lost/unconfirmed receipt не разрешает удаление по одному имени; rollback не удаляет IPv4 с совпавшим IPv6 ID. IPv6 RSC export использует /ipv6 address add +advertise=no, parser распознаёт IPv6 address line. Multiple/special address import и export явно отклоняются вместо тихой потери данных. Это single-address literal import/export, не произвольный RouterOS configuration codec. Существующий ReUI create sheet получил подсказку IPv4/IPv6; структура/Frame/base-nova сохранены. + +При native приёмке обнаружено: getEnabledServerById ранее проверял только ID. Теперь общий getter допускает только enabled=true и полный корректный DB ID; WireGuard selected catalog также исключает disabled. Это влияет на использующих getter GRE/WireGuard/firewall/containers/traffic/overlay. Постоянный opt-in test network-disabled-server.test.ts делает33 API/SQL cases: три типа Home Router/Jump Host/Exit Node ×11 calls, factory любого ROS client запрещена, contacts=0, enabled positive control проходит.8 malformed IDs проверяются до SQL, включая unsafe/выход за integer DB range. Без MMAPP_OWN_TEST_DB SQL часть честно skipped; в собственном PG стенде выполнена и база удалена. + +Проверки:56 node:test cases import/rollback/revision/delete, service/config/HTTP scripts, GRE config-sync и14 invalid GRE requests, containers/live-maps profiles;10 E2E (WireGuard IPv6/error-retained input PC/mobile, tables/menus и GRE smoke после общего planner), source types/ESLint; обе production сборки,984 emitted outputs совпали с окончательным backend. + +Native изолированная PG + production handlers + оба CHR:29 API cases, native handshake0s и Ping3/3 в обе стороны после IPv6 invalid=false; create/PATCH/IPv6→IPv4/clear, multiple-address history restore, legacy409, export multiple409, RSC/conf single IPv6 export и реальный import/delete, disabled GET empty/PATCH404 с forbidden factory. После каждого запуска9 static collections×2 совпали, собственные интерфейсы/peers/адреса и PG базы удалены. Native source/cleanup не меняли пароли/public/default routes. Background polling главного приложения продолжался; отсутствие всех background SQL writes не заявляется. + +Первый сценарий остановился из-за ошибки теста: ожидал200 от POST peer, фактический корректный201. После исправления сценарий прошёл весь native lifecycle, но завершающий disabled PATCH вернул500 из-за реального дефекта общего getter. Оба отрицательных протокола сохранены; getter исправлен,33 isolated API/SQL cases и полный native сценарий на финальном образе прошли. Frontend повторно не собирался из-за backend-only gate delta. + +Runtime backend/frontend ux-wg152 healthy; deployed live read-only catalog/create-sheet на1440/320 passed после cleanup. Browser mutations покрыты controlled E2E, реальные RouterOS mutations — production handlers в выделенной базе. IPv6 ULA inner по IPv4 WireGuard outer, не IPv6 public WAN/client OS/throughput/scale/lost ACK physical acceptance. From-pool/EUI-64/multiple-address import/export остаются явными ограничениями; full regression после139 не повторялась. + +Протоколы: [native/cleanup](ux-ui/acceptance/wireguard-ipv6-native-2026-10-11.json), [API/history/import/export](ux-ui/acceptance/wireguard-ipv6-api-history-2026-10-11.json), [33 SQL/API gates](ux-ui/acceptance/network-disabled-sql-2026-10-11.json), [до исправления getter](ux-ui/acceptance/wireguard-ipv6-before-gate-api-2026-10-11.json), [compiled](ux-ui/acceptance/wireguard-ipv6-compiled-2026-10-11.json), [live UI](ux-ui/acceptance/wireguard-ipv6-smoke-2026-10-11.json). + +Следующий153 — sustained OSPF/BFD-only gate и recovery на собственных GRE;12.5 остаётся открытым. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 48c37931..b2d1e018 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-11: завершён12.5.151 — GRE IPv6 static-address lifecycle через приложение: чтение/create/PATCH/history restore/delete, дополнительные/выключенные адреса, family-safe IDs и legacy409.15 профильных E2E, source types/ESLint, обе production сборки, 984 compiled parity и real native API/SQL/Ping3/3×2/cleanup прошли. Runtime backend/frontend ux-gre151 healthy;12.5 открыт. Следующий152 — application WireGuard IPv6 address lifecycle, включая импорт/историю/удаление; работать укрупнённым разделом без повторения принятого GRE. +Актуальное продолжение 2026-10-11: завершён12.5.152 — WireGuard IPv6 static lifecycle/create/PATCH/history/delete и single-address RSC/conf export/import.29 real API cases, native handshake/Ping3/3×2,33 disabled-server API/SQL cases всех трёх типов с zero ROS contacts,56 backend tests/профили GRE и контейнеров/live maps,10 E2E/types/ESLint/builds/984 parity/live PC/mobile passed. Runtime backend/frontend ux-wg152 healthy;12.5 открыт. Следующий153 — OSPF/BFD sustained gate: BFD-only loss при сохранённых OSPF Hello на собственных GRE, восстановление и independent cleanup. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -2954,3 +2954,23 @@ Create/PATCH/restore/delete выбирают /ip/address или /ipv6/address п Протоколы: [native/cleanup](ux-ui/acceptance/gre-ipv6-native-2026-10-11.json), [API/history](ux-ui/acceptance/gre-ipv6-api-history-2026-10-11.json), [первый запуск](ux-ui/acceptance/gre-ipv6-initial-cleanup-2026-10-11.json), [compiled parity](ux-ui/acceptance/gre-ipv6-compiled-2026-10-11.json), [live UI](ux-ui/acceptance/gre-ipv6-smoke-2026-10-11.json). Границы: IPv6 ULA inner по IPv4 GRE outer; IPv6 outer/public WAN, scale, длительное восстановление и from-pool/EUI-64 не приняты. Полная регрессия после139 не повторялась: выполнен профиль изменённых областей. Следующий152 — WireGuard IPv6 lifecycle;12.5 остаётся открытым. + +## 12.5.152 — IPv6 lifecycle WireGuard и общий допуск выключенных серверов (2026-10-11) + +WireGuard читает оба address catalogs; ошибки/не-массивы не превращаются в пустые данные. Catalog показывает активный статический адрес, новые snapshots сохраняют все статические адреса с disabled/advertise/comment/no-dad. Dynamic link-local исключён из управления. Create и PATCH address, history restore и delete выбирают семейство по CIDR; дополнительные адреса сохранены, legacy snapshot с несколькими текущими адресами409. Общий адресный planner переиспользован GRE/WireGuard; constructor alias GreRestoreConflict совместим. PATCH не переименовывает одновременно интерфейс и адрес. + +Import подтверждает receipts в /ip/address и /ipv6/address раздельно, normalized IPv6 identities принимаются, automatic link-local не считается чужим static child. Lost/unconfirmed receipt не разрешает удаление по одному имени; rollback не удаляет IPv4 с совпавшим IPv6 ID. IPv6 RSC export использует /ipv6 address add +advertise=no, parser распознаёт IPv6 address line. Multiple/special address import и export явно отклоняются вместо тихой потери данных. Это single-address literal import/export, не произвольный RouterOS configuration codec. Существующий ReUI create sheet получил подсказку IPv4/IPv6; структура/Frame/base-nova сохранены. + +При native приёмке обнаружено: getEnabledServerById ранее проверял только ID. Теперь общий getter допускает только enabled=true и полный корректный DB ID; WireGuard selected catalog также исключает disabled. Это влияет на использующих getter GRE/WireGuard/firewall/containers/traffic/overlay. Постоянный opt-in test network-disabled-server.test.ts делает33 API/SQL cases: три типа Home Router/Jump Host/Exit Node ×11 calls, factory любого ROS client запрещена, contacts=0, enabled positive control проходит.8 malformed IDs проверяются до SQL, включая unsafe/выход за integer DB range. Без MMAPP_OWN_TEST_DB SQL часть честно skipped; в собственном PG стенде выполнена и база удалена. + +Проверки:56 node:test cases import/rollback/revision/delete, service/config/HTTP scripts, GRE config-sync и14 invalid GRE requests, containers/live-maps profiles;10 E2E (WireGuard IPv6/error-retained input PC/mobile, tables/menus и GRE smoke после общего planner), source types/ESLint; обе production сборки,984 emitted outputs совпали с окончательным backend. + +Native изолированная PG + production handlers + оба CHR:29 API cases, native handshake0s и Ping3/3 в обе стороны после IPv6 invalid=false; create/PATCH/IPv6→IPv4/clear, multiple-address history restore, legacy409, export multiple409, RSC/conf single IPv6 export и реальный import/delete, disabled GET empty/PATCH404 с forbidden factory. После каждого запуска9 static collections×2 совпали, собственные интерфейсы/peers/адреса и PG базы удалены. Native source/cleanup не меняли пароли/public/default routes. Background polling главного приложения продолжался; отсутствие всех background SQL writes не заявляется. + +Первый сценарий остановился из-за ошибки теста: ожидал200 от POST peer, фактический корректный201. После исправления сценарий прошёл весь native lifecycle, но завершающий disabled PATCH вернул500 из-за реального дефекта общего getter. Оба отрицательных протокола сохранены; getter исправлен,33 isolated API/SQL cases и полный native сценарий на финальном образе прошли. Frontend повторно не собирался из-за backend-only gate delta. + +Runtime backend/frontend ux-wg152 healthy; deployed live read-only catalog/create-sheet на1440/320 passed после cleanup. Browser mutations покрыты controlled E2E, реальные RouterOS mutations — production handlers в выделенной базе. IPv6 ULA inner по IPv4 WireGuard outer, не IPv6 public WAN/client OS/throughput/scale/lost ACK physical acceptance. From-pool/EUI-64/multiple-address import/export остаются явными ограничениями; full regression после139 не повторялась. + +Протоколы: [native/cleanup](ux-ui/acceptance/wireguard-ipv6-native-2026-10-11.json), [API/history/import/export](ux-ui/acceptance/wireguard-ipv6-api-history-2026-10-11.json), [33 SQL/API gates](ux-ui/acceptance/network-disabled-sql-2026-10-11.json), [до исправления getter](ux-ui/acceptance/wireguard-ipv6-before-gate-api-2026-10-11.json), [compiled](ux-ui/acceptance/wireguard-ipv6-compiled-2026-10-11.json), [live UI](ux-ui/acceptance/wireguard-ipv6-smoke-2026-10-11.json). + +Следующий153 — sustained OSPF/BFD-only gate и recovery на собственных GRE;12.5 остаётся открытым. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index 8b6c24fa..99cd5f09 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-10 -Сервер161.104.46.222: завершён12.5.151 — GRE IPv6 static-address lifecycle через приложение: чтение/create/PATCH/history restore/delete, дополнительные/выключенные адреса, family-safe IDs и legacy409.15 профильных E2E, source types/ESLint, обе production сборки, 984 compiled parity и real native API/SQL/Ping3/3×2/cleanup прошли. Runtime backend/frontend ux-gre151 healthy;12.5 открыт. Следующий152 — application WireGuard IPv6 address lifecycle, включая импорт/историю/удаление; работать укрупнённым разделом без повторения принятого GRE. +Сервер161.104.46.222: завершён12.5.152 — WireGuard IPv6 static lifecycle/create/PATCH/history/delete и single-address RSC/conf export/import.29 real API cases, native handshake/Ping3/3×2,33 disabled-server API/SQL cases всех трёх типов с zero ROS contacts,56 backend tests/профили GRE и контейнеров/live maps,10 E2E/types/ESLint/builds/984 parity/live PC/mobile passed. Runtime backend/frontend ux-wg152 healthy;12.5 открыт. Следующий153 — OSPF/BFD sustained gate: BFD-only loss при сохранённых OSPF Hello на собственных GRE, восстановление и independent cleanup. | Параметр | Проверенное состояние | | --- | --- | @@ -14,7 +14,7 @@ | 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-gre151 и mmapp-test/frontend:ux-gre151; запущены | +| Образы приложения | mmapp-test/backend:ux-wg152 и mmapp-test/frontend:ux-wg152; запущены | Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example). @@ -1380,3 +1380,23 @@ Create/PATCH/restore/delete выбирают /ip/address или /ipv6/address п Протоколы: [native/cleanup](ux-ui/acceptance/gre-ipv6-native-2026-10-11.json), [API/history](ux-ui/acceptance/gre-ipv6-api-history-2026-10-11.json), [первый запуск](ux-ui/acceptance/gre-ipv6-initial-cleanup-2026-10-11.json), [compiled parity](ux-ui/acceptance/gre-ipv6-compiled-2026-10-11.json), [live UI](ux-ui/acceptance/gre-ipv6-smoke-2026-10-11.json). Границы: IPv6 ULA inner по IPv4 GRE outer; IPv6 outer/public WAN, scale, длительное восстановление и from-pool/EUI-64 не приняты. Полная регрессия после139 не повторялась: выполнен профиль изменённых областей. Следующий152 — WireGuard IPv6 lifecycle;12.5 остаётся открытым. + +## 12.5.152 — IPv6 lifecycle WireGuard и общий допуск выключенных серверов (2026-10-11) + +WireGuard читает оба address catalogs; ошибки/не-массивы не превращаются в пустые данные. Catalog показывает активный статический адрес, новые snapshots сохраняют все статические адреса с disabled/advertise/comment/no-dad. Dynamic link-local исключён из управления. Create и PATCH address, history restore и delete выбирают семейство по CIDR; дополнительные адреса сохранены, legacy snapshot с несколькими текущими адресами409. Общий адресный planner переиспользован GRE/WireGuard; constructor alias GreRestoreConflict совместим. PATCH не переименовывает одновременно интерфейс и адрес. + +Import подтверждает receipts в /ip/address и /ipv6/address раздельно, normalized IPv6 identities принимаются, automatic link-local не считается чужим static child. Lost/unconfirmed receipt не разрешает удаление по одному имени; rollback не удаляет IPv4 с совпавшим IPv6 ID. IPv6 RSC export использует /ipv6 address add +advertise=no, parser распознаёт IPv6 address line. Multiple/special address import и export явно отклоняются вместо тихой потери данных. Это single-address literal import/export, не произвольный RouterOS configuration codec. Существующий ReUI create sheet получил подсказку IPv4/IPv6; структура/Frame/base-nova сохранены. + +При native приёмке обнаружено: getEnabledServerById ранее проверял только ID. Теперь общий getter допускает только enabled=true и полный корректный DB ID; WireGuard selected catalog также исключает disabled. Это влияет на использующих getter GRE/WireGuard/firewall/containers/traffic/overlay. Постоянный opt-in test network-disabled-server.test.ts делает33 API/SQL cases: три типа Home Router/Jump Host/Exit Node ×11 calls, factory любого ROS client запрещена, contacts=0, enabled positive control проходит.8 malformed IDs проверяются до SQL, включая unsafe/выход за integer DB range. Без MMAPP_OWN_TEST_DB SQL часть честно skipped; в собственном PG стенде выполнена и база удалена. + +Проверки:56 node:test cases import/rollback/revision/delete, service/config/HTTP scripts, GRE config-sync и14 invalid GRE requests, containers/live-maps profiles;10 E2E (WireGuard IPv6/error-retained input PC/mobile, tables/menus и GRE smoke после общего planner), source types/ESLint; обе production сборки,984 emitted outputs совпали с окончательным backend. + +Native изолированная PG + production handlers + оба CHR:29 API cases, native handshake0s и Ping3/3 в обе стороны после IPv6 invalid=false; create/PATCH/IPv6→IPv4/clear, multiple-address history restore, legacy409, export multiple409, RSC/conf single IPv6 export и реальный import/delete, disabled GET empty/PATCH404 с forbidden factory. После каждого запуска9 static collections×2 совпали, собственные интерфейсы/peers/адреса и PG базы удалены. Native source/cleanup не меняли пароли/public/default routes. Background polling главного приложения продолжался; отсутствие всех background SQL writes не заявляется. + +Первый сценарий остановился из-за ошибки теста: ожидал200 от POST peer, фактический корректный201. После исправления сценарий прошёл весь native lifecycle, но завершающий disabled PATCH вернул500 из-за реального дефекта общего getter. Оба отрицательных протокола сохранены; getter исправлен,33 isolated API/SQL cases и полный native сценарий на финальном образе прошли. Frontend повторно не собирался из-за backend-only gate delta. + +Runtime backend/frontend ux-wg152 healthy; deployed live read-only catalog/create-sheet на1440/320 passed после cleanup. Browser mutations покрыты controlled E2E, реальные RouterOS mutations — production handlers в выделенной базе. IPv6 ULA inner по IPv4 WireGuard outer, не IPv6 public WAN/client OS/throughput/scale/lost ACK physical acceptance. From-pool/EUI-64/multiple-address import/export остаются явными ограничениями; full regression после139 не повторялась. + +Протоколы: [native/cleanup](ux-ui/acceptance/wireguard-ipv6-native-2026-10-11.json), [API/history/import/export](ux-ui/acceptance/wireguard-ipv6-api-history-2026-10-11.json), [33 SQL/API gates](ux-ui/acceptance/network-disabled-sql-2026-10-11.json), [до исправления getter](ux-ui/acceptance/wireguard-ipv6-before-gate-api-2026-10-11.json), [compiled](ux-ui/acceptance/wireguard-ipv6-compiled-2026-10-11.json), [live UI](ux-ui/acceptance/wireguard-ipv6-smoke-2026-10-11.json). + +Следующий153 — sustained OSPF/BFD-only gate и recovery на собственных GRE;12.5 остаётся открытым. diff --git a/docs/ux-ui/acceptance/network-disabled-sql-2026-10-11.json b/docs/ux-ui/acceptance/network-disabled-sql-2026-10-11.json new file mode 100644 index 00000000..5d9e8998 --- /dev/null +++ b/docs/ux-ui/acceptance/network-disabled-sql-2026-10-11.json @@ -0,0 +1,12 @@ +{ + "date": "2026-10-11", + "name": "mm_accept_152_1791731008028", + "tests": [ + { + "module": "network-disabled152", + "passed": true, + "output": "network-disabled-server.test.ts: 33 API/SQL checks for three types, zero RouterOS contacts, enabled positive control passed\n" + } + ], + "databaseRemoved": true +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/wireguard-ipv6-api-history-2026-10-11.json b/docs/ux-ui/acceptance/wireguard-ipv6-api-history-2026-10-11.json new file mode 100644 index 00000000..6809ac2c --- /dev/null +++ b/docs/ux-ui/acceptance/wireguard-ipv6-api-history-2026-10-11.json @@ -0,0 +1,185 @@ +{ + "date": "2026-10-11", + "block": "12.5.152", + "name": "mm-wg152-1791731017548", + "cases": [ + { + "method": "POST", + "path": "/wireguard/interfaces", + "status": 201 + }, + { + "method": "POST", + "path": "/wireguard/interfaces", + "status": 201 + }, + { + "method": "POST", + "path": "/wireguard/peers", + "status": 201 + }, + { + "method": "POST", + "path": "/wireguard/peers", + "status": 201 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard/revisions?serverId=1", + "status": 200 + }, + { + "method": "PATCH", + "path": "/wireguard/interfaces/1/*97", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard/revisions?serverId=1", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/export", + "status": 409 + }, + { + "method": "POST", + "path": "/wireguard/revisions/legacy-mm_accept_152_1791731017548/restore", + "status": 409 + }, + { + "method": "POST", + "path": "/wireguard/revisions/1de6597b-eceb-4f17-8d16-2600471cb08f/restore", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/revisions/780d0a98-d237-4822-8502-9410b8f1e0fc/restore", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/revisions/1de6597b-eceb-4f17-8d16-2600471cb08f/restore", + "status": 200 + }, + { + "method": "PATCH", + "path": "/wireguard/interfaces/1/*97", + "status": 200 + }, + { + "method": "PATCH", + "path": "/wireguard/interfaces/1/*97", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/revisions/1de6597b-eceb-4f17-8d16-2600471cb08f/restore", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/export", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/export", + "status": 200 + }, + { + "method": "DELETE", + "path": "/wireguard/interfaces/1/*97", + "status": 200 + }, + { + "method": "DELETE", + "path": "/wireguard/interfaces/2/*76", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/import", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + }, + { + "method": "DELETE", + "path": "/wireguard/interfaces/1/*98", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/import", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + }, + { + "method": "DELETE", + "path": "/wireguard/interfaces/1/*99", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + }, + { + "method": "PATCH", + "path": "/wireguard/interfaces/1/*97", + "status": 404 + } + ], + "pings": [ + { + "serverId": 1, + "sent": 3, + "received": 3 + }, + { + "serverId": 2, + "sent": 3, + "received": 3 + } + ], + "ready": [ + { + "serverId": 1, + "addressInvalid": "false" + }, + { + "serverId": 2, + "addressInvalid": "false" + } + ], + "handshakes": [ + { + "serverId": 1, + "lastHandshake": "0s" + }, + { + "serverId": 2, + "lastHandshake": "0s" + } + ], + "passed": true +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/wireguard-ipv6-before-gate-api-2026-10-11.json b/docs/ux-ui/acceptance/wireguard-ipv6-before-gate-api-2026-10-11.json new file mode 100644 index 00000000..f42a7595 --- /dev/null +++ b/docs/ux-ui/acceptance/wireguard-ipv6-before-gate-api-2026-10-11.json @@ -0,0 +1,179 @@ +{ + "date": "2026-10-11", + "block": "12.5.152", + "name": "mm-wg152-1791730555296", + "cases": [ + { + "method": "POST", + "path": "/wireguard/interfaces", + "status": 201 + }, + { + "method": "POST", + "path": "/wireguard/interfaces", + "status": 201 + }, + { + "method": "POST", + "path": "/wireguard/peers", + "status": 201 + }, + { + "method": "POST", + "path": "/wireguard/peers", + "status": 201 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard/revisions?serverId=1", + "status": 200 + }, + { + "method": "PATCH", + "path": "/wireguard/interfaces/1/*94", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard/revisions?serverId=1", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/export", + "status": 409 + }, + { + "method": "POST", + "path": "/wireguard/revisions/legacy-mm_accept_152_1791730555296/restore", + "status": 409 + }, + { + "method": "POST", + "path": "/wireguard/revisions/420f83f6-f319-4bf9-9eb8-d37b8acb49d5/restore", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/revisions/52635299-be9a-4d64-b7e0-e2e268af5811/restore", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/revisions/420f83f6-f319-4bf9-9eb8-d37b8acb49d5/restore", + "status": 200 + }, + { + "method": "PATCH", + "path": "/wireguard/interfaces/1/*94", + "status": 200 + }, + { + "method": "PATCH", + "path": "/wireguard/interfaces/1/*94", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/revisions/420f83f6-f319-4bf9-9eb8-d37b8acb49d5/restore", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/export", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/export", + "status": 200 + }, + { + "method": "DELETE", + "path": "/wireguard/interfaces/1/*94", + "status": 200 + }, + { + "method": "DELETE", + "path": "/wireguard/interfaces/2/*75", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/import", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + }, + { + "method": "DELETE", + "path": "/wireguard/interfaces/1/*95", + "status": 200 + }, + { + "method": "POST", + "path": "/wireguard/import", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + }, + { + "method": "DELETE", + "path": "/wireguard/interfaces/1/*96", + "status": 200 + }, + { + "method": "GET", + "path": "/wireguard?serverId=1", + "status": 200 + } + ], + "pings": [ + { + "serverId": 1, + "sent": 3, + "received": 3 + }, + { + "serverId": 2, + "sent": 3, + "received": 3 + } + ], + "ready": [ + { + "serverId": 1, + "addressInvalid": "false" + }, + { + "serverId": 2, + "addressInvalid": "false" + } + ], + "handshakes": [ + { + "serverId": 1, + "lastHandshake": "0s" + }, + { + "serverId": 2, + "lastHandshake": "0s" + } + ] +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/wireguard-ipv6-before-gate-cleanup-2026-10-11.json b/docs/ux-ui/acceptance/wireguard-ipv6-before-gate-cleanup-2026-10-11.json new file mode 100644 index 00000000..69b5c3bb --- /dev/null +++ b/docs/ux-ui/acceptance/wireguard-ipv6-before-gate-cleanup-2026-10-11.json @@ -0,0 +1,49 @@ +{ + "date": "2026-10-11", + "block": "12.5.152", + "name": "mm-wg152-1791730555296", + "devices": [ + { + "serverId": 1, + "host": "194.226.121.150", + "version": "7.24.5 (stable)", + "reachable": true + }, + { + "serverId": 2, + "host": "104.165.244.7", + "version": "7.24.5 (stable)", + "reachable": true + } + ], + "cleanup": [ + { + "serverId": 1, + "/interface/wireguard/peers": true, + "/interface/wireguard": true, + "/ip/address": true, + "/ipv6/address": true, + "/ip/route": true, + "/ipv6/route": true, + "/ip/firewall/filter": true, + "/ip/firewall/nat": true, + "/ipv6/firewall/filter": true + }, + { + "serverId": 2, + "/interface/wireguard/peers": true, + "/interface/wireguard": true, + "/ip/address": true, + "/ipv6/address": true, + "/ip/route": true, + "/ipv6/route": true, + "/ip/firewall/filter": true, + "/ip/firewall/nat": true, + "/ipv6/firewall/filter": true + } + ], + "childExit": 1, + "error": "Child failed: inspect private log\n\n1 !== 0\n", + "databaseRemoved": true, + "passed": false +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/wireguard-ipv6-compiled-2026-10-11.json b/docs/ux-ui/acceptance/wireguard-ipv6-compiled-2026-10-11.json new file mode 100644 index 00000000..f562c4ac --- /dev/null +++ b/docs/ux-ui/acceptance/wireguard-ipv6-compiled-2026-10-11.json @@ -0,0 +1,994 @@ +{ + "date": "2026-10-11", + "block": "12.5.152", + "backendVersion": "1.0.0-test.wg152", + "normalization": "CRLF to LF; emitted files derived from tracked non-test source inputs", + "matched": [ + "backend/dist/config.js", + "backend/dist/config.js.map", + "backend/dist/config.d.ts", + "backend/dist/config.d.ts.map", + "backend/dist/constants/ping-probe.js", + "backend/dist/constants/ping-probe.js.map", + "backend/dist/constants/ping-probe.d.ts", + "backend/dist/constants/ping-probe.d.ts.map", + "backend/dist/db/bootstrap.js", + "backend/dist/db/bootstrap.js.map", + "backend/dist/db/bootstrap.d.ts", + "backend/dist/db/bootstrap.d.ts.map", + "backend/dist/db/index.js", + "backend/dist/db/index.js.map", + "backend/dist/db/index.d.ts", + "backend/dist/db/index.d.ts.map", + "backend/dist/db/json.js", + "backend/dist/db/json.js.map", + "backend/dist/db/json.d.ts", + "backend/dist/db/json.d.ts.map", + "backend/dist/db/migrate.js", + "backend/dist/db/migrate.js.map", + "backend/dist/db/migrate.d.ts", + "backend/dist/db/migrate.d.ts.map", + "backend/dist/db/operation-lock.js", + "backend/dist/db/operation-lock.js.map", + "backend/dist/db/operation-lock.d.ts", + "backend/dist/db/operation-lock.d.ts.map", + "backend/dist/db/partitions.js", + "backend/dist/db/partitions.js.map", + "backend/dist/db/partitions.d.ts", + "backend/dist/db/partitions.d.ts.map", + "backend/dist/db/schema.js", + "backend/dist/db/schema.js.map", + "backend/dist/db/schema.d.ts", + "backend/dist/db/schema.d.ts.map", + "backend/dist/db/sql-bind.js", + "backend/dist/db/sql-bind.js.map", + "backend/dist/db/sql-bind.d.ts", + "backend/dist/db/sql-bind.d.ts.map", + "backend/dist/db/sqlite-import.js", + "backend/dist/db/sqlite-import.js.map", + "backend/dist/db/sqlite-import.d.ts", + "backend/dist/db/sqlite-import.d.ts.map", + "backend/dist/db/traffic-flags.js", + "backend/dist/db/traffic-flags.js.map", + "backend/dist/db/traffic-flags.d.ts", + "backend/dist/db/traffic-flags.d.ts.map", + "backend/dist/index.js", + "backend/dist/index.js.map", + "backend/dist/index.d.ts", + "backend/dist/index.d.ts.map", + "backend/dist/lib/permissions.js", + "backend/dist/lib/permissions.js.map", + "backend/dist/lib/permissions.d.ts", + "backend/dist/lib/permissions.d.ts.map", + "backend/dist/managed-markers.js", + "backend/dist/managed-markers.js.map", + "backend/dist/managed-markers.d.ts", + "backend/dist/managed-markers.d.ts.map", + "backend/dist/modules/events/repository/events-repository.js", + "backend/dist/modules/events/repository/events-repository.js.map", + "backend/dist/modules/events/repository/events-repository.d.ts", + "backend/dist/modules/events/repository/events-repository.d.ts.map", + "backend/dist/modules/events/service/events-service.js", + "backend/dist/modules/events/service/events-service.js.map", + "backend/dist/modules/events/service/events-service.d.ts", + "backend/dist/modules/events/service/events-service.d.ts.map", + "backend/dist/modules/servers/mapper/servers-mapper.js", + "backend/dist/modules/servers/mapper/servers-mapper.js.map", + "backend/dist/modules/servers/mapper/servers-mapper.d.ts", + "backend/dist/modules/servers/mapper/servers-mapper.d.ts.map", + "backend/dist/modules/servers/repository/servers-repository.js", + "backend/dist/modules/servers/repository/servers-repository.js.map", + "backend/dist/modules/servers/repository/servers-repository.d.ts", + "backend/dist/modules/servers/repository/servers-repository.d.ts.map", + "backend/dist/modules/servers/service/servers-service.js", + "backend/dist/modules/servers/service/servers-service.js.map", + "backend/dist/modules/servers/service/servers-service.d.ts", + "backend/dist/modules/servers/service/servers-service.d.ts.map", + "backend/dist/modules/users/iface-type.js", + "backend/dist/modules/users/iface-type.js.map", + "backend/dist/modules/users/iface-type.d.ts", + "backend/dist/modules/users/iface-type.d.ts.map", + "backend/dist/modules/users/interface-inventory.js", + "backend/dist/modules/users/interface-inventory.js.map", + "backend/dist/modules/users/interface-inventory.d.ts", + "backend/dist/modules/users/interface-inventory.d.ts.map", + "backend/dist/modules/users/peer-bind.js", + "backend/dist/modules/users/peer-bind.js.map", + "backend/dist/modules/users/peer-bind.d.ts", + "backend/dist/modules/users/peer-bind.d.ts.map", + "backend/dist/modules/users/repository/users-repository.js", + "backend/dist/modules/users/repository/users-repository.js.map", + "backend/dist/modules/users/repository/users-repository.d.ts", + "backend/dist/modules/users/repository/users-repository.d.ts.map", + "backend/dist/modules/users/service/users-service.js", + "backend/dist/modules/users/service/users-service.js.map", + "backend/dist/modules/users/service/users-service.d.ts", + "backend/dist/modules/users/service/users-service.d.ts.map", + "backend/dist/plugins/auth.js", + "backend/dist/plugins/auth.js.map", + "backend/dist/plugins/auth.d.ts", + "backend/dist/plugins/auth.d.ts.map", + "backend/dist/plugins/database-request-gate.js", + "backend/dist/plugins/database-request-gate.js.map", + "backend/dist/plugins/database-request-gate.d.ts", + "backend/dist/plugins/database-request-gate.d.ts.map", + "backend/dist/routes/alerts.js", + "backend/dist/routes/alerts.js.map", + "backend/dist/routes/alerts.d.ts", + "backend/dist/routes/alerts.d.ts.map", + "backend/dist/routes/backups.js", + "backend/dist/routes/backups.js.map", + "backend/dist/routes/backups.d.ts", + "backend/dist/routes/backups.d.ts.map", + "backend/dist/routes/bgp.js", + "backend/dist/routes/bgp.js.map", + "backend/dist/routes/bgp.d.ts", + "backend/dist/routes/bgp.d.ts.map", + "backend/dist/routes/certificates.js", + "backend/dist/routes/certificates.js.map", + "backend/dist/routes/certificates.d.ts", + "backend/dist/routes/certificates.d.ts.map", + "backend/dist/routes/containers.js", + "backend/dist/routes/containers.js.map", + "backend/dist/routes/containers.d.ts", + "backend/dist/routes/containers.d.ts.map", + "backend/dist/routes/events.js", + "backend/dist/routes/events.js.map", + "backend/dist/routes/events.d.ts", + "backend/dist/routes/events.d.ts.map", + "backend/dist/routes/evobgp-catalog.js", + "backend/dist/routes/evobgp-catalog.js.map", + "backend/dist/routes/evobgp-catalog.d.ts", + "backend/dist/routes/evobgp-catalog.d.ts.map", + "backend/dist/routes/evobgp-settings.js", + "backend/dist/routes/evobgp-settings.js.map", + "backend/dist/routes/evobgp-settings.d.ts", + "backend/dist/routes/evobgp-settings.d.ts.map", + "backend/dist/routes/evobgp.js", + "backend/dist/routes/evobgp.js.map", + "backend/dist/routes/evobgp.d.ts", + "backend/dist/routes/evobgp.d.ts.map", + "backend/dist/routes/exec.js", + "backend/dist/routes/exec.js.map", + "backend/dist/routes/exec.d.ts", + "backend/dist/routes/exec.d.ts.map", + "backend/dist/routes/filters.js", + "backend/dist/routes/filters.js.map", + "backend/dist/routes/filters.d.ts", + "backend/dist/routes/filters.d.ts.map", + "backend/dist/routes/firewall.js", + "backend/dist/routes/firewall.js.map", + "backend/dist/routes/firewall.d.ts", + "backend/dist/routes/firewall.d.ts.map", + "backend/dist/routes/geoip.js", + "backend/dist/routes/geoip.js.map", + "backend/dist/routes/geoip.d.ts", + "backend/dist/routes/geoip.d.ts.map", + "backend/dist/routes/gre.js", + "backend/dist/routes/gre.js.map", + "backend/dist/routes/gre.d.ts", + "backend/dist/routes/gre.d.ts.map", + "backend/dist/routes/internet-path.js", + "backend/dist/routes/internet-path.js.map", + "backend/dist/routes/internet-path.d.ts", + "backend/dist/routes/internet-path.d.ts.map", + "backend/dist/routes/ipsec.js", + "backend/dist/routes/ipsec.js.map", + "backend/dist/routes/ipsec.d.ts", + "backend/dist/routes/ipsec.d.ts.map", + "backend/dist/routes/network.js", + "backend/dist/routes/network.js.map", + "backend/dist/routes/network.d.ts", + "backend/dist/routes/network.d.ts.map", + "backend/dist/routes/ospf.js", + "backend/dist/routes/ospf.js.map", + "backend/dist/routes/ospf.d.ts", + "backend/dist/routes/ospf.d.ts.map", + "backend/dist/routes/ping-probes-write.js", + "backend/dist/routes/ping-probes-write.js.map", + "backend/dist/routes/ping-probes-write.d.ts", + "backend/dist/routes/ping-probes-write.d.ts.map", + "backend/dist/routes/probes.js", + "backend/dist/routes/probes.js.map", + "backend/dist/routes/probes.d.ts", + "backend/dist/routes/probes.d.ts.map", + "backend/dist/routes/recursive-routes.js", + "backend/dist/routes/recursive-routes.js.map", + "backend/dist/routes/recursive-routes.d.ts", + "backend/dist/routes/recursive-routes.d.ts.map", + "backend/dist/routes/scheduler.js", + "backend/dist/routes/scheduler.js.map", + "backend/dist/routes/scheduler.d.ts", + "backend/dist/routes/scheduler.d.ts.map", + "backend/dist/routes/servers-api-ping.js", + "backend/dist/routes/servers-api-ping.js.map", + "backend/dist/routes/servers-api-ping.d.ts", + "backend/dist/routes/servers-api-ping.d.ts.map", + "backend/dist/routes/servers.js", + "backend/dist/routes/servers.js.map", + "backend/dist/routes/servers.d.ts", + "backend/dist/routes/servers.d.ts.map", + "backend/dist/routes/sidebar-counts.js", + "backend/dist/routes/sidebar-counts.js.map", + "backend/dist/routes/sidebar-counts.d.ts", + "backend/dist/routes/sidebar-counts.d.ts.map", + "backend/dist/routes/speed-probes-write.js", + "backend/dist/routes/speed-probes-write.js.map", + "backend/dist/routes/speed-probes-write.d.ts", + "backend/dist/routes/speed-probes-write.d.ts.map", + "backend/dist/routes/speed-test.js", + "backend/dist/routes/speed-test.js.map", + "backend/dist/routes/speed-test.d.ts", + "backend/dist/routes/speed-test.d.ts.map", + "backend/dist/routes/statistics.js", + "backend/dist/routes/statistics.js.map", + "backend/dist/routes/statistics.d.ts", + "backend/dist/routes/statistics.d.ts.map", + "backend/dist/routes/system-database.js", + "backend/dist/routes/system-database.js.map", + "backend/dist/routes/system-database.d.ts", + "backend/dist/routes/system-database.d.ts.map", + "backend/dist/routes/traffic-flow-purge.js", + "backend/dist/routes/traffic-flow-purge.js.map", + "backend/dist/routes/traffic-flow-purge.d.ts", + "backend/dist/routes/traffic-flow-purge.d.ts.map", + "backend/dist/routes/traffic-flow-settings.js", + "backend/dist/routes/traffic-flow-settings.js.map", + "backend/dist/routes/traffic-flow-settings.d.ts", + "backend/dist/routes/traffic-flow-settings.d.ts.map", + "backend/dist/routes/traffic-flow.js", + "backend/dist/routes/traffic-flow.js.map", + "backend/dist/routes/traffic-flow.d.ts", + "backend/dist/routes/traffic-flow.d.ts.map", + "backend/dist/routes/traffic.js", + "backend/dist/routes/traffic.js.map", + "backend/dist/routes/traffic.d.ts", + "backend/dist/routes/traffic.d.ts.map", + "backend/dist/routes/uptime.js", + "backend/dist/routes/uptime.js.map", + "backend/dist/routes/uptime.d.ts", + "backend/dist/routes/uptime.d.ts.map", + "backend/dist/routes/users.js", + "backend/dist/routes/users.js.map", + "backend/dist/routes/users.d.ts", + "backend/dist/routes/users.d.ts.map", + "backend/dist/routes/vxlan.js", + "backend/dist/routes/vxlan.js.map", + "backend/dist/routes/vxlan.d.ts", + "backend/dist/routes/vxlan.d.ts.map", + "backend/dist/routes/wireguard.js", + "backend/dist/routes/wireguard.js.map", + "backend/dist/routes/wireguard.d.ts", + "backend/dist/routes/wireguard.d.ts.map", + "backend/dist/scripts/migrate-sqlite-to-pg.js", + "backend/dist/scripts/migrate-sqlite-to-pg.js.map", + "backend/dist/scripts/migrate-sqlite-to-pg.d.ts", + "backend/dist/scripts/migrate-sqlite-to-pg.d.ts.map", + "backend/dist/scripts/rebuild-flow-facts.js", + "backend/dist/scripts/rebuild-flow-facts.js.map", + "backend/dist/scripts/rebuild-flow-facts.d.ts", + "backend/dist/scripts/rebuild-flow-facts.d.ts.map", + "backend/dist/services/acme-cloudflare.js", + "backend/dist/services/acme-cloudflare.js.map", + "backend/dist/services/acme-cloudflare.d.ts", + "backend/dist/services/acme-cloudflare.d.ts.map", + "backend/dist/services/alert-collector-hooks.js", + "backend/dist/services/alert-collector-hooks.js.map", + "backend/dist/services/alert-collector-hooks.d.ts", + "backend/dist/services/alert-collector-hooks.d.ts.map", + "backend/dist/services/alert-engine/confirm-stability.js", + "backend/dist/services/alert-engine/confirm-stability.js.map", + "backend/dist/services/alert-engine/confirm-stability.d.ts", + "backend/dist/services/alert-engine/confirm-stability.d.ts.map", + "backend/dist/services/alert-engine/cooldown.js", + "backend/dist/services/alert-engine/cooldown.js.map", + "backend/dist/services/alert-engine/cooldown.d.ts", + "backend/dist/services/alert-engine/cooldown.d.ts.map", + "backend/dist/services/alert-engine/decision-engine.js", + "backend/dist/services/alert-engine/decision-engine.js.map", + "backend/dist/services/alert-engine/decision-engine.d.ts", + "backend/dist/services/alert-engine/decision-engine.d.ts.map", + "backend/dist/services/alert-engine/evaluate-rule.js", + "backend/dist/services/alert-engine/evaluate-rule.js.map", + "backend/dist/services/alert-engine/evaluate-rule.d.ts", + "backend/dist/services/alert-engine/evaluate-rule.d.ts.map", + "backend/dist/services/alert-engine/groups.js", + "backend/dist/services/alert-engine/groups.js.map", + "backend/dist/services/alert-engine/groups.d.ts", + "backend/dist/services/alert-engine/groups.d.ts.map", + "backend/dist/services/alert-engine/outbox-execution.js", + "backend/dist/services/alert-engine/outbox-execution.js.map", + "backend/dist/services/alert-engine/outbox-execution.d.ts", + "backend/dist/services/alert-engine/outbox-execution.d.ts.map", + "backend/dist/services/alert-engine/outbox.js", + "backend/dist/services/alert-engine/outbox.js.map", + "backend/dist/services/alert-engine/outbox.d.ts", + "backend/dist/services/alert-engine/outbox.d.ts.map", + "backend/dist/services/alert-engine/prev-live-store.js", + "backend/dist/services/alert-engine/prev-live-store.js.map", + "backend/dist/services/alert-engine/prev-live-store.d.ts", + "backend/dist/services/alert-engine/prev-live-store.d.ts.map", + "backend/dist/services/alert-engine/rule-matcher.js", + "backend/dist/services/alert-engine/rule-matcher.js.map", + "backend/dist/services/alert-engine/rule-matcher.d.ts", + "backend/dist/services/alert-engine/rule-matcher.d.ts.map", + "backend/dist/services/alert-engine/run-once.js", + "backend/dist/services/alert-engine/run-once.js.map", + "backend/dist/services/alert-engine/run-once.d.ts", + "backend/dist/services/alert-engine/run-once.d.ts.map", + "backend/dist/services/alert-engine/signal-ingestor.js", + "backend/dist/services/alert-engine/signal-ingestor.js.map", + "backend/dist/services/alert-engine/signal-ingestor.d.ts", + "backend/dist/services/alert-engine/signal-ingestor.d.ts.map", + "backend/dist/services/alert-engine/signals-from-collectors.js", + "backend/dist/services/alert-engine/signals-from-collectors.js.map", + "backend/dist/services/alert-engine/signals-from-collectors.d.ts", + "backend/dist/services/alert-engine/signals-from-collectors.d.ts.map", + "backend/dist/services/alert-engine/snapshot.js", + "backend/dist/services/alert-engine/snapshot.js.map", + "backend/dist/services/alert-engine/snapshot.d.ts", + "backend/dist/services/alert-engine/snapshot.d.ts.map", + "backend/dist/services/alert-engine/source-watermark.js", + "backend/dist/services/alert-engine/source-watermark.js.map", + "backend/dist/services/alert-engine/source-watermark.d.ts", + "backend/dist/services/alert-engine/source-watermark.d.ts.map", + "backend/dist/services/alert-engine/state-machine.js", + "backend/dist/services/alert-engine/state-machine.js.map", + "backend/dist/services/alert-engine/state-machine.d.ts", + "backend/dist/services/alert-engine/state-machine.d.ts.map", + "backend/dist/services/alert-engine/telegram-emoji.js", + "backend/dist/services/alert-engine/telegram-emoji.js.map", + "backend/dist/services/alert-engine/telegram-emoji.d.ts", + "backend/dist/services/alert-engine/telegram-emoji.d.ts.map", + "backend/dist/services/alert-engine/types.js", + "backend/dist/services/alert-engine/types.js.map", + "backend/dist/services/alert-engine/types.d.ts", + "backend/dist/services/alert-engine/types.d.ts.map", + "backend/dist/services/alerts-config-execution.js", + "backend/dist/services/alerts-config-execution.js.map", + "backend/dist/services/alerts-config-execution.d.ts", + "backend/dist/services/alerts-config-execution.d.ts.map", + "backend/dist/services/alerts-outbox-overview.js", + "backend/dist/services/alerts-outbox-overview.js.map", + "backend/dist/services/alerts-outbox-overview.d.ts", + "backend/dist/services/alerts-outbox-overview.d.ts.map", + "backend/dist/services/alerts-outbox-retry.js", + "backend/dist/services/alerts-outbox-retry.js.map", + "backend/dist/services/alerts-outbox-retry.d.ts", + "backend/dist/services/alerts-outbox-retry.d.ts.map", + "backend/dist/services/alerts-overview.js", + "backend/dist/services/alerts-overview.js.map", + "backend/dist/services/alerts-overview.d.ts", + "backend/dist/services/alerts-overview.d.ts.map", + "backend/dist/services/alerts-revision.js", + "backend/dist/services/alerts-revision.js.map", + "backend/dist/services/alerts-revision.d.ts", + "backend/dist/services/alerts-revision.d.ts.map", + "backend/dist/services/alerts-service.js", + "backend/dist/services/alerts-service.js.map", + "backend/dist/services/alerts-service.d.ts", + "backend/dist/services/alerts-service.d.ts.map", + "backend/dist/services/alerts-telegram-delivery.js", + "backend/dist/services/alerts-telegram-delivery.js.map", + "backend/dist/services/alerts-telegram-delivery.d.ts", + "backend/dist/services/alerts-telegram-delivery.d.ts.map", + "backend/dist/services/alerts-telegram-send.js", + "backend/dist/services/alerts-telegram-send.js.map", + "backend/dist/services/alerts-telegram-send.d.ts", + "backend/dist/services/alerts-telegram-send.d.ts.map", + "backend/dist/services/alerts-telegram-settings.js", + "backend/dist/services/alerts-telegram-settings.js.map", + "backend/dist/services/alerts-telegram-settings.d.ts", + "backend/dist/services/alerts-telegram-settings.d.ts.map", + "backend/dist/services/backup-deletion.js", + "backend/dist/services/backup-deletion.js.map", + "backend/dist/services/backup-deletion.d.ts", + "backend/dist/services/backup-deletion.d.ts.map", + "backend/dist/services/backup-dto.js", + "backend/dist/services/backup-dto.js.map", + "backend/dist/services/backup-dto.d.ts", + "backend/dist/services/backup-dto.d.ts.map", + "backend/dist/services/backup-job-execution.js", + "backend/dist/services/backup-job-execution.js.map", + "backend/dist/services/backup-job-execution.d.ts", + "backend/dist/services/backup-job-execution.d.ts.map", + "backend/dist/services/backup-prune.js", + "backend/dist/services/backup-prune.js.map", + "backend/dist/services/backup-prune.d.ts", + "backend/dist/services/backup-prune.d.ts.map", + "backend/dist/services/backup-restore.js", + "backend/dist/services/backup-restore.js.map", + "backend/dist/services/backup-restore.d.ts", + "backend/dist/services/backup-restore.d.ts.map", + "backend/dist/services/backup-scheduler-collector.js", + "backend/dist/services/backup-scheduler-collector.js.map", + "backend/dist/services/backup-scheduler-collector.d.ts", + "backend/dist/services/backup-scheduler-collector.d.ts.map", + "backend/dist/services/backup-scheduler-run.js", + "backend/dist/services/backup-scheduler-run.js.map", + "backend/dist/services/backup-scheduler-run.d.ts", + "backend/dist/services/backup-scheduler-run.d.ts.map", + "backend/dist/services/backup-service.js", + "backend/dist/services/backup-service.js.map", + "backend/dist/services/backup-service.d.ts", + "backend/dist/services/backup-service.d.ts.map", + "backend/dist/services/backup-storage-update.js", + "backend/dist/services/backup-storage-update.js.map", + "backend/dist/services/backup-storage-update.d.ts", + "backend/dist/services/backup-storage-update.d.ts.map", + "backend/dist/services/bandwidth-test-rates.js", + "backend/dist/services/bandwidth-test-rates.js.map", + "backend/dist/services/bandwidth-test-rates.d.ts", + "backend/dist/services/bandwidth-test-rates.d.ts.map", + "backend/dist/services/bandwidth-test-status.js", + "backend/dist/services/bandwidth-test-status.js.map", + "backend/dist/services/bandwidth-test-status.d.ts", + "backend/dist/services/bandwidth-test-status.d.ts.map", + "backend/dist/services/bgp-catalog-poll.js", + "backend/dist/services/bgp-catalog-poll.js.map", + "backend/dist/services/bgp-catalog-poll.d.ts", + "backend/dist/services/bgp-catalog-poll.d.ts.map", + "backend/dist/services/bgp-parse-sessions.js", + "backend/dist/services/bgp-parse-sessions.js.map", + "backend/dist/services/bgp-parse-sessions.d.ts", + "backend/dist/services/bgp-parse-sessions.d.ts.map", + "backend/dist/services/bgp-peers-live.js", + "backend/dist/services/bgp-peers-live.js.map", + "backend/dist/services/bgp-peers-live.d.ts", + "backend/dist/services/bgp-peers-live.d.ts.map", + "backend/dist/services/btest-node-locks.js", + "backend/dist/services/btest-node-locks.js.map", + "backend/dist/services/btest-node-locks.d.ts", + "backend/dist/services/btest-node-locks.d.ts.map", + "backend/dist/services/certificate-issue-dto.js", + "backend/dist/services/certificate-issue-dto.js.map", + "backend/dist/services/certificate-issue-dto.d.ts", + "backend/dist/services/certificate-issue-dto.d.ts.map", + "backend/dist/services/certificate-issue-execution.js", + "backend/dist/services/certificate-issue-execution.js.map", + "backend/dist/services/certificate-issue-execution.d.ts", + "backend/dist/services/certificate-issue-execution.d.ts.map", + "backend/dist/services/certificate-issue-request.js", + "backend/dist/services/certificate-issue-request.js.map", + "backend/dist/services/certificate-issue-request.d.ts", + "backend/dist/services/certificate-issue-request.d.ts.map", + "backend/dist/services/certificate-issue-runner.js", + "backend/dist/services/certificate-issue-runner.js.map", + "backend/dist/services/certificate-issue-runner.d.ts", + "backend/dist/services/certificate-issue-runner.d.ts.map", + "backend/dist/services/certificate-parse.js", + "backend/dist/services/certificate-parse.js.map", + "backend/dist/services/certificate-parse.d.ts", + "backend/dist/services/certificate-parse.d.ts.map", + "backend/dist/services/certificate-renew-collector.js", + "backend/dist/services/certificate-renew-collector.js.map", + "backend/dist/services/certificate-renew-collector.d.ts", + "backend/dist/services/certificate-renew-collector.d.ts.map", + "backend/dist/services/certificates-service.js", + "backend/dist/services/certificates-service.js.map", + "backend/dist/services/certificates-service.d.ts", + "backend/dist/services/certificates-service.d.ts.map", + "backend/dist/services/certificates-state.js", + "backend/dist/services/certificates-state.js.map", + "backend/dist/services/certificates-state.d.ts", + "backend/dist/services/certificates-state.d.ts.map", + "backend/dist/services/complete-count.js", + "backend/dist/services/complete-count.js.map", + "backend/dist/services/complete-count.d.ts", + "backend/dist/services/complete-count.d.ts.map", + "backend/dist/services/config-apply-plan.js", + "backend/dist/services/config-apply-plan.js.map", + "backend/dist/services/config-apply-plan.d.ts", + "backend/dist/services/config-apply-plan.d.ts.map", + "backend/dist/services/config-revisions.js", + "backend/dist/services/config-revisions.js.map", + "backend/dist/services/config-revisions.d.ts", + "backend/dist/services/config-revisions.d.ts.map", + "backend/dist/services/containers-live.js", + "backend/dist/services/containers-live.js.map", + "backend/dist/services/containers-live.d.ts", + "backend/dist/services/containers-live.d.ts.map", + "backend/dist/services/containers-state.js", + "backend/dist/services/containers-state.js.map", + "backend/dist/services/containers-state.d.ts", + "backend/dist/services/containers-state.d.ts.map", + "backend/dist/services/database-background-work.js", + "backend/dist/services/database-background-work.js.map", + "backend/dist/services/database-background-work.d.ts", + "backend/dist/services/database-background-work.d.ts.map", + "backend/dist/services/database-request-gate.js", + "backend/dist/services/database-request-gate.js.map", + "backend/dist/services/database-request-gate.d.ts", + "backend/dist/services/database-request-gate.d.ts.map", + "backend/dist/services/entity-snapshots.js", + "backend/dist/services/entity-snapshots.js.map", + "backend/dist/services/entity-snapshots.d.ts", + "backend/dist/services/entity-snapshots.d.ts.map", + "backend/dist/services/evobgp-catalog.js", + "backend/dist/services/evobgp-catalog.js.map", + "backend/dist/services/evobgp-catalog.d.ts", + "backend/dist/services/evobgp-catalog.d.ts.map", + "backend/dist/services/firewall-live.js", + "backend/dist/services/firewall-live.js.map", + "backend/dist/services/firewall-live.d.ts", + "backend/dist/services/firewall-live.d.ts.map", + "backend/dist/services/firewall-poll.js", + "backend/dist/services/firewall-poll.js.map", + "backend/dist/services/firewall-poll.d.ts", + "backend/dist/services/firewall-poll.d.ts.map", + "backend/dist/services/geoip-settings.js", + "backend/dist/services/geoip-settings.js.map", + "backend/dist/services/geoip-settings.d.ts", + "backend/dist/services/geoip-settings.d.ts.map", + "backend/dist/services/geoip-update-collector.js", + "backend/dist/services/geoip-update-collector.js.map", + "backend/dist/services/geoip-update-collector.d.ts", + "backend/dist/services/geoip-update-collector.d.ts.map", + "backend/dist/services/gre-bgp-snapshot-collector.js", + "backend/dist/services/gre-bgp-snapshot-collector.js.map", + "backend/dist/services/gre-bgp-snapshot-collector.d.ts", + "backend/dist/services/gre-bgp-snapshot-collector.d.ts.map", + "backend/dist/services/gre-catalog-poll.js", + "backend/dist/services/gre-catalog-poll.js.map", + "backend/dist/services/gre-catalog-poll.d.ts", + "backend/dist/services/gre-catalog-poll.d.ts.map", + "backend/dist/services/gre-comment.js", + "backend/dist/services/gre-comment.js.map", + "backend/dist/services/gre-comment.d.ts", + "backend/dist/services/gre-comment.d.ts.map", + "backend/dist/services/gre-live.js", + "backend/dist/services/gre-live.js.map", + "backend/dist/services/gre-live.d.ts", + "backend/dist/services/gre-live.d.ts.map", + "backend/dist/services/gre-tunnels-live.js", + "backend/dist/services/gre-tunnels-live.js.map", + "backend/dist/services/gre-tunnels-live.d.ts", + "backend/dist/services/gre-tunnels-live.d.ts.map", + "backend/dist/services/internet-path-collector.js", + "backend/dist/services/internet-path-collector.js.map", + "backend/dist/services/internet-path-collector.d.ts", + "backend/dist/services/internet-path-collector.d.ts.map", + "backend/dist/services/internet-path-snapshot.js", + "backend/dist/services/internet-path-snapshot.js.map", + "backend/dist/services/internet-path-snapshot.d.ts", + "backend/dist/services/internet-path-snapshot.d.ts.map", + "backend/dist/services/ipsec-ca.js", + "backend/dist/services/ipsec-ca.js.map", + "backend/dist/services/ipsec-ca.d.ts", + "backend/dist/services/ipsec-ca.d.ts.map", + "backend/dist/services/ipsec-certificate-dn.js", + "backend/dist/services/ipsec-certificate-dn.js.map", + "backend/dist/services/ipsec-certificate-dn.d.ts", + "backend/dist/services/ipsec-certificate-dn.d.ts.map", + "backend/dist/services/ipsec-cleanup.js", + "backend/dist/services/ipsec-cleanup.js.map", + "backend/dist/services/ipsec-cleanup.d.ts", + "backend/dist/services/ipsec-cleanup.d.ts.map", + "backend/dist/services/ipsec-config.js", + "backend/dist/services/ipsec-config.js.map", + "backend/dist/services/ipsec-config.d.ts", + "backend/dist/services/ipsec-config.d.ts.map", + "backend/dist/services/ipsec-live.js", + "backend/dist/services/ipsec-live.js.map", + "backend/dist/services/ipsec-live.d.ts", + "backend/dist/services/ipsec-live.d.ts.map", + "backend/dist/services/ipsec-operation.js", + "backend/dist/services/ipsec-operation.js.map", + "backend/dist/services/ipsec-operation.d.ts", + "backend/dist/services/ipsec-operation.d.ts.map", + "backend/dist/services/ipsec-ros.js", + "backend/dist/services/ipsec-ros.js.map", + "backend/dist/services/ipsec-ros.d.ts", + "backend/dist/services/ipsec-ros.d.ts.map", + "backend/dist/services/mikrotik.js", + "backend/dist/services/mikrotik.js.map", + "backend/dist/services/mikrotik.d.ts", + "backend/dist/services/mikrotik.d.ts.map", + "backend/dist/services/monitoring-server-catalog.js", + "backend/dist/services/monitoring-server-catalog.js.map", + "backend/dist/services/monitoring-server-catalog.d.ts", + "backend/dist/services/monitoring-server-catalog.d.ts.map", + "backend/dist/services/ospf-catalog-poll.js", + "backend/dist/services/ospf-catalog-poll.js.map", + "backend/dist/services/ospf-catalog-poll.d.ts", + "backend/dist/services/ospf-catalog-poll.d.ts.map", + "backend/dist/services/ospf-effective-disabled.js", + "backend/dist/services/ospf-effective-disabled.js.map", + "backend/dist/services/ospf-effective-disabled.d.ts", + "backend/dist/services/ospf-effective-disabled.d.ts.map", + "backend/dist/services/ospf-observed-values.js", + "backend/dist/services/ospf-observed-values.js.map", + "backend/dist/services/ospf-observed-values.d.ts", + "backend/dist/services/ospf-observed-values.d.ts.map", + "backend/dist/services/ospf-optimizer-identity.js", + "backend/dist/services/ospf-optimizer-identity.js.map", + "backend/dist/services/ospf-optimizer-identity.d.ts", + "backend/dist/services/ospf-optimizer-identity.d.ts.map", + "backend/dist/services/ospf-optimizer-metrics.js", + "backend/dist/services/ospf-optimizer-metrics.js.map", + "backend/dist/services/ospf-optimizer-metrics.d.ts", + "backend/dist/services/ospf-optimizer-metrics.d.ts.map", + "backend/dist/services/ospf-optimizer-plan.js", + "backend/dist/services/ospf-optimizer-plan.js.map", + "backend/dist/services/ospf-optimizer-plan.d.ts", + "backend/dist/services/ospf-optimizer-plan.d.ts.map", + "backend/dist/services/ospf-route-parse.js", + "backend/dist/services/ospf-route-parse.js.map", + "backend/dist/services/ospf-route-parse.d.ts", + "backend/dist/services/ospf-route-parse.d.ts.map", + "backend/dist/services/ping-probe-replacement.js", + "backend/dist/services/ping-probe-replacement.js.map", + "backend/dist/services/ping-probe-replacement.d.ts", + "backend/dist/services/ping-probe-replacement.d.ts.map", + "backend/dist/services/poller.js", + "backend/dist/services/poller.js.map", + "backend/dist/services/poller.d.ts", + "backend/dist/services/poller.d.ts.map", + "backend/dist/services/recursive-apply.js", + "backend/dist/services/recursive-apply.js.map", + "backend/dist/services/recursive-apply.d.ts", + "backend/dist/services/recursive-apply.d.ts.map", + "backend/dist/services/recursive-route-catalog.js", + "backend/dist/services/recursive-route-catalog.js.map", + "backend/dist/services/recursive-route-catalog.d.ts", + "backend/dist/services/recursive-route-catalog.d.ts.map", + "backend/dist/services/recursive-route-validation.js", + "backend/dist/services/recursive-route-validation.js.map", + "backend/dist/services/recursive-route-validation.d.ts", + "backend/dist/services/recursive-route-validation.d.ts.map", + "backend/dist/services/ros-metric-parse.js", + "backend/dist/services/ros-metric-parse.js.map", + "backend/dist/services/ros-metric-parse.d.ts", + "backend/dist/services/ros-metric-parse.d.ts.map", + "backend/dist/services/ros-ops.js", + "backend/dist/services/ros-ops.js.map", + "backend/dist/services/ros-ops.d.ts", + "backend/dist/services/ros-ops.d.ts.map", + "backend/dist/services/s3-backup-client.js", + "backend/dist/services/s3-backup-client.js.map", + "backend/dist/services/s3-backup-client.d.ts", + "backend/dist/services/s3-backup-client.d.ts.map", + "backend/dist/services/scheduler-recorded-run.js", + "backend/dist/services/scheduler-recorded-run.js.map", + "backend/dist/services/scheduler-recorded-run.d.ts", + "backend/dist/services/scheduler-recorded-run.d.ts.map", + "backend/dist/services/scheduler-running.js", + "backend/dist/services/scheduler-running.js.map", + "backend/dist/services/scheduler-running.d.ts", + "backend/dist/services/scheduler-running.d.ts.map", + "backend/dist/services/scheduler.js", + "backend/dist/services/scheduler.js.map", + "backend/dist/services/scheduler.d.ts", + "backend/dist/services/scheduler.d.ts.map", + "backend/dist/services/serial-operation.js", + "backend/dist/services/serial-operation.js.map", + "backend/dist/services/serial-operation.d.ts", + "backend/dist/services/serial-operation.d.ts.map", + "backend/dist/services/servers-rest-ping-collector.js", + "backend/dist/services/servers-rest-ping-collector.js.map", + "backend/dist/services/servers-rest-ping-collector.d.ts", + "backend/dist/services/servers-rest-ping-collector.d.ts.map", + "backend/dist/services/speed-history-dto.js", + "backend/dist/services/speed-history-dto.js.map", + "backend/dist/services/speed-history-dto.d.ts", + "backend/dist/services/speed-history-dto.d.ts.map", + "backend/dist/services/speed-probe-dto.js", + "backend/dist/services/speed-probe-dto.js.map", + "backend/dist/services/speed-probe-dto.d.ts", + "backend/dist/services/speed-probe-dto.d.ts.map", + "backend/dist/services/speed-probe-replacement.js", + "backend/dist/services/speed-probe-replacement.js.map", + "backend/dist/services/speed-probe-replacement.d.ts", + "backend/dist/services/speed-probe-replacement.d.ts.map", + "backend/dist/services/speed-run-error-message.js", + "backend/dist/services/speed-run-error-message.js.map", + "backend/dist/services/speed-run-error-message.d.ts", + "backend/dist/services/speed-run-error-message.d.ts.map", + "backend/dist/services/speed-run-persistence.js", + "backend/dist/services/speed-run-persistence.js.map", + "backend/dist/services/speed-run-persistence.d.ts", + "backend/dist/services/speed-run-persistence.d.ts.map", + "backend/dist/services/statistics-aggregate.js", + "backend/dist/services/statistics-aggregate.js.map", + "backend/dist/services/statistics-aggregate.d.ts", + "backend/dist/services/statistics-aggregate.d.ts.map", + "backend/dist/services/system-database-backup.js", + "backend/dist/services/system-database-backup.js.map", + "backend/dist/services/system-database-backup.d.ts", + "backend/dist/services/system-database-backup.d.ts.map", + "backend/dist/services/system-database-cache.js", + "backend/dist/services/system-database-cache.js.map", + "backend/dist/services/system-database-cache.d.ts", + "backend/dist/services/system-database-cache.d.ts.map", + "backend/dist/services/system-database-operation.js", + "backend/dist/services/system-database-operation.js.map", + "backend/dist/services/system-database-operation.d.ts", + "backend/dist/services/system-database-operation.d.ts.map", + "backend/dist/services/system-database-partition-restore.js", + "backend/dist/services/system-database-partition-restore.js.map", + "backend/dist/services/system-database-partition-restore.d.ts", + "backend/dist/services/system-database-partition-restore.d.ts.map", + "backend/dist/services/system-database-restore-execution.js", + "backend/dist/services/system-database-restore-execution.js.map", + "backend/dist/services/system-database-restore-execution.d.ts", + "backend/dist/services/system-database-restore-execution.d.ts.map", + "backend/dist/services/traffic-collector.js", + "backend/dist/services/traffic-collector.js.map", + "backend/dist/services/traffic-collector.d.ts", + "backend/dist/services/traffic-collector.d.ts.map", + "backend/dist/services/traffic-flow-analytics.js", + "backend/dist/services/traffic-flow-analytics.js.map", + "backend/dist/services/traffic-flow-analytics.d.ts", + "backend/dist/services/traffic-flow-analytics.d.ts.map", + "backend/dist/services/traffic-flow-apps.js", + "backend/dist/services/traffic-flow-apps.js.map", + "backend/dist/services/traffic-flow-apps.d.ts", + "backend/dist/services/traffic-flow-apps.d.ts.map", + "backend/dist/services/traffic-flow-brands.js", + "backend/dist/services/traffic-flow-brands.js.map", + "backend/dist/services/traffic-flow-brands.d.ts", + "backend/dist/services/traffic-flow-brands.d.ts.map", + "backend/dist/services/traffic-flow-classify.js", + "backend/dist/services/traffic-flow-classify.js.map", + "backend/dist/services/traffic-flow-classify.d.ts", + "backend/dist/services/traffic-flow-classify.d.ts.map", + "backend/dist/services/traffic-flow-collector-ipc.js", + "backend/dist/services/traffic-flow-collector-ipc.js.map", + "backend/dist/services/traffic-flow-collector-ipc.d.ts", + "backend/dist/services/traffic-flow-collector-ipc.d.ts.map", + "backend/dist/services/traffic-flow-collector-worker.js", + "backend/dist/services/traffic-flow-collector-worker.js.map", + "backend/dist/services/traffic-flow-collector-worker.d.ts", + "backend/dist/services/traffic-flow-collector-worker.d.ts.map", + "backend/dist/services/traffic-flow-dedup.js", + "backend/dist/services/traffic-flow-dedup.js.map", + "backend/dist/services/traffic-flow-dedup.d.ts", + "backend/dist/services/traffic-flow-dedup.d.ts.map", + "backend/dist/services/traffic-flow-dest.js", + "backend/dist/services/traffic-flow-dest.js.map", + "backend/dist/services/traffic-flow-dest.d.ts", + "backend/dist/services/traffic-flow-dest.d.ts.map", + "backend/dist/services/traffic-flow-engine.js", + "backend/dist/services/traffic-flow-engine.js.map", + "backend/dist/services/traffic-flow-engine.d.ts", + "backend/dist/services/traffic-flow-engine.d.ts.map", + "backend/dist/services/traffic-flow-facts-filter.js", + "backend/dist/services/traffic-flow-facts-filter.js.map", + "backend/dist/services/traffic-flow-facts-filter.d.ts", + "backend/dist/services/traffic-flow-facts-filter.d.ts.map", + "backend/dist/services/traffic-flow-facts-rebuild.js", + "backend/dist/services/traffic-flow-facts-rebuild.js.map", + "backend/dist/services/traffic-flow-facts-rebuild.d.ts", + "backend/dist/services/traffic-flow-facts-rebuild.d.ts.map", + "backend/dist/services/traffic-flow-facts.js", + "backend/dist/services/traffic-flow-facts.js.map", + "backend/dist/services/traffic-flow-facts.d.ts", + "backend/dist/services/traffic-flow-facts.d.ts.map", + "backend/dist/services/traffic-flow-geoip.js", + "backend/dist/services/traffic-flow-geoip.js.map", + "backend/dist/services/traffic-flow-geoip.d.ts", + "backend/dist/services/traffic-flow-geoip.d.ts.map", + "backend/dist/services/traffic-flow-host-file-builders.js", + "backend/dist/services/traffic-flow-host-file-builders.js.map", + "backend/dist/services/traffic-flow-host-file-builders.d.ts", + "backend/dist/services/traffic-flow-host-file-builders.d.ts.map", + "backend/dist/services/traffic-flow-host-files.js", + "backend/dist/services/traffic-flow-host-files.js.map", + "backend/dist/services/traffic-flow-host-files.d.ts", + "backend/dist/services/traffic-flow-host-files.d.ts.map", + "backend/dist/services/traffic-flow-ifaces.js", + "backend/dist/services/traffic-flow-ifaces.js.map", + "backend/dist/services/traffic-flow-ifaces.d.ts", + "backend/dist/services/traffic-flow-ifaces.d.ts.map", + "backend/dist/services/traffic-flow-ifindex.js", + "backend/dist/services/traffic-flow-ifindex.js.map", + "backend/dist/services/traffic-flow-ifindex.d.ts", + "backend/dist/services/traffic-flow-ifindex.d.ts.map", + "backend/dist/services/traffic-flow-ingest.js", + "backend/dist/services/traffic-flow-ingest.js.map", + "backend/dist/services/traffic-flow-ingest.d.ts", + "backend/dist/services/traffic-flow-ingest.d.ts.map", + "backend/dist/services/traffic-flow-ip.js", + "backend/dist/services/traffic-flow-ip.js.map", + "backend/dist/services/traffic-flow-ip.d.ts", + "backend/dist/services/traffic-flow-ip.d.ts.map", + "backend/dist/services/traffic-flow-key-execution.js", + "backend/dist/services/traffic-flow-key-execution.js.map", + "backend/dist/services/traffic-flow-key-execution.d.ts", + "backend/dist/services/traffic-flow-key-execution.d.ts.map", + "backend/dist/services/traffic-flow-map-exporter.js", + "backend/dist/services/traffic-flow-map-exporter.js.map", + "backend/dist/services/traffic-flow-map-exporter.d.ts", + "backend/dist/services/traffic-flow-map-exporter.d.ts.map", + "backend/dist/services/traffic-flow-map-hops.js", + "backend/dist/services/traffic-flow-map-hops.js.map", + "backend/dist/services/traffic-flow-map-hops.d.ts", + "backend/dist/services/traffic-flow-map-hops.d.ts.map", + "backend/dist/services/traffic-flow-overlay.js", + "backend/dist/services/traffic-flow-overlay.js.map", + "backend/dist/services/traffic-flow-overlay.d.ts", + "backend/dist/services/traffic-flow-overlay.d.ts.map", + "backend/dist/services/traffic-flow-parse.js", + "backend/dist/services/traffic-flow-parse.js.map", + "backend/dist/services/traffic-flow-parse.d.ts", + "backend/dist/services/traffic-flow-parse.d.ts.map", + "backend/dist/services/traffic-flow-planes.js", + "backend/dist/services/traffic-flow-planes.js.map", + "backend/dist/services/traffic-flow-planes.d.ts", + "backend/dist/services/traffic-flow-planes.d.ts.map", + "backend/dist/services/traffic-flow-purge-execution.js", + "backend/dist/services/traffic-flow-purge-execution.js.map", + "backend/dist/services/traffic-flow-purge-execution.d.ts", + "backend/dist/services/traffic-flow-purge-execution.d.ts.map", + "backend/dist/services/traffic-flow-ripe.js", + "backend/dist/services/traffic-flow-ripe.js.map", + "backend/dist/services/traffic-flow-ripe.d.ts", + "backend/dist/services/traffic-flow-ripe.d.ts.map", + "backend/dist/services/traffic-flow-settings.js", + "backend/dist/services/traffic-flow-settings.js.map", + "backend/dist/services/traffic-flow-settings.d.ts", + "backend/dist/services/traffic-flow-settings.d.ts.map", + "backend/dist/services/traffic-flow-topology.js", + "backend/dist/services/traffic-flow-topology.js.map", + "backend/dist/services/traffic-flow-topology.d.ts", + "backend/dist/services/traffic-flow-topology.d.ts.map", + "backend/dist/services/traffic-flow-worker-shutdown.js", + "backend/dist/services/traffic-flow-worker-shutdown.js.map", + "backend/dist/services/traffic-flow-worker-shutdown.d.ts", + "backend/dist/services/traffic-flow-worker-shutdown.d.ts.map", + "backend/dist/services/traffic-rate.js", + "backend/dist/services/traffic-rate.js.map", + "backend/dist/services/traffic-rate.d.ts", + "backend/dist/services/traffic-rate.d.ts.map", + "backend/dist/services/traffic-users.js", + "backend/dist/services/traffic-users.js.map", + "backend/dist/services/traffic-users.d.ts", + "backend/dist/services/traffic-users.d.ts.map", + "backend/dist/services/uptime-collector.js", + "backend/dist/services/uptime-collector.js.map", + "backend/dist/services/uptime-collector.d.ts", + "backend/dist/services/uptime-collector.d.ts.map", + "backend/dist/services/uptime-probe-observations.js", + "backend/dist/services/uptime-probe-observations.js.map", + "backend/dist/services/uptime-probe-observations.d.ts", + "backend/dist/services/uptime-probe-observations.d.ts.map", + "backend/dist/services/uptime-speed-service.js", + "backend/dist/services/uptime-speed-service.js.map", + "backend/dist/services/uptime-speed-service.d.ts", + "backend/dist/services/uptime-speed-service.d.ts.map", + "backend/dist/services/vxlan-live.js", + "backend/dist/services/vxlan-live.js.map", + "backend/dist/services/vxlan-live.d.ts", + "backend/dist/services/vxlan-live.d.ts.map", + "backend/dist/services/wg-keys.js", + "backend/dist/services/wg-keys.js.map", + "backend/dist/services/wg-keys.d.ts", + "backend/dist/services/wg-keys.d.ts.map", + "backend/dist/services/wireguard-config.js", + "backend/dist/services/wireguard-config.js.map", + "backend/dist/services/wireguard-config.d.ts", + "backend/dist/services/wireguard-config.d.ts.map", + "backend/dist/services/wireguard-delete.js", + "backend/dist/services/wireguard-delete.js.map", + "backend/dist/services/wireguard-delete.d.ts", + "backend/dist/services/wireguard-delete.d.ts.map", + "backend/dist/services/wireguard-import-apply.js", + "backend/dist/services/wireguard-import-apply.js.map", + "backend/dist/services/wireguard-import-apply.d.ts", + "backend/dist/services/wireguard-import-apply.d.ts.map", + "backend/dist/services/wireguard-import-revision.js", + "backend/dist/services/wireguard-import-revision.js.map", + "backend/dist/services/wireguard-import-revision.d.ts", + "backend/dist/services/wireguard-import-revision.d.ts.map", + "backend/dist/services/wireguard-import.js", + "backend/dist/services/wireguard-import.js.map", + "backend/dist/services/wireguard-import.d.ts", + "backend/dist/services/wireguard-import.d.ts.map", + "backend/dist/services/wireguard-live.js", + "backend/dist/services/wireguard-live.js.map", + "backend/dist/services/wireguard-live.d.ts", + "backend/dist/services/wireguard-live.d.ts.map", + "backend/dist/services/wireguard-observed-values.js", + "backend/dist/services/wireguard-observed-values.js.map", + "backend/dist/services/wireguard-observed-values.d.ts", + "backend/dist/services/wireguard-observed-values.d.ts.map", + "backend/dist/services/wireguard-ros.js", + "backend/dist/services/wireguard-ros.js.map", + "backend/dist/services/wireguard-ros.d.ts", + "backend/dist/services/wireguard-ros.d.ts.map", + "backend/dist/test/pg.js", + "backend/dist/test/pg.js.map", + "backend/dist/test/pg.d.ts", + "backend/dist/test/pg.d.ts.map", + "backend/dist/types/scheduler-run-snapshot.js", + "backend/dist/types/scheduler-run-snapshot.js.map", + "backend/dist/types/scheduler-run-snapshot.d.ts", + "backend/dist/types/scheduler-run-snapshot.d.ts.map", + "backend/dist/types/server.js", + "backend/dist/types/server.js.map", + "backend/dist/types/server.d.ts", + "backend/dist/types/server.d.ts.map", + "backend/dist/utils/abort-signals.js", + "backend/dist/utils/abort-signals.js.map", + "backend/dist/utils/abort-signals.d.ts", + "backend/dist/utils/abort-signals.d.ts.map", + "backend/dist/utils/ros-src-address.js", + "backend/dist/utils/ros-src-address.js.map", + "backend/dist/utils/ros-src-address.d.ts", + "backend/dist/utils/ros-src-address.d.ts.map", + "backend/dist/utils/server-id.js", + "backend/dist/utils/server-id.js.map", + "backend/dist/utils/server-id.d.ts", + "backend/dist/utils/server-id.d.ts.map", + "packages/contracts/dist/alerts.js", + "packages/contracts/dist/alerts.js.map", + "packages/contracts/dist/alerts.d.ts", + "packages/contracts/dist/alerts.d.ts.map", + "packages/contracts/dist/backups.js", + "packages/contracts/dist/backups.js.map", + "packages/contracts/dist/backups.d.ts", + "packages/contracts/dist/backups.d.ts.map", + "packages/contracts/dist/certificates.js", + "packages/contracts/dist/certificates.js.map", + "packages/contracts/dist/certificates.d.ts", + "packages/contracts/dist/certificates.d.ts.map", + "packages/contracts/dist/containers.js", + "packages/contracts/dist/containers.js.map", + "packages/contracts/dist/containers.d.ts", + "packages/contracts/dist/containers.d.ts.map", + "packages/contracts/dist/events.js", + "packages/contracts/dist/events.js.map", + "packages/contracts/dist/events.d.ts", + "packages/contracts/dist/events.d.ts.map", + "packages/contracts/dist/evobgp.js", + "packages/contracts/dist/evobgp.js.map", + "packages/contracts/dist/evobgp.d.ts", + "packages/contracts/dist/evobgp.d.ts.map", + "packages/contracts/dist/geoip.js", + "packages/contracts/dist/geoip.js.map", + "packages/contracts/dist/geoip.d.ts", + "packages/contracts/dist/geoip.d.ts.map", + "packages/contracts/dist/index.js", + "packages/contracts/dist/index.js.map", + "packages/contracts/dist/index.d.ts", + "packages/contracts/dist/index.d.ts.map", + "packages/contracts/dist/ipsec.js", + "packages/contracts/dist/ipsec.js.map", + "packages/contracts/dist/ipsec.d.ts", + "packages/contracts/dist/ipsec.d.ts.map", + "packages/contracts/dist/ospf-optimizer.js", + "packages/contracts/dist/ospf-optimizer.js.map", + "packages/contracts/dist/ospf-optimizer.d.ts", + "packages/contracts/dist/ospf-optimizer.d.ts.map", + "packages/contracts/dist/servers.js", + "packages/contracts/dist/servers.js.map", + "packages/contracts/dist/servers.d.ts", + "packages/contracts/dist/servers.d.ts.map", + "packages/contracts/dist/statistics.js", + "packages/contracts/dist/statistics.js.map", + "packages/contracts/dist/statistics.d.ts", + "packages/contracts/dist/statistics.d.ts.map", + "packages/contracts/dist/traffic-flow.js", + "packages/contracts/dist/traffic-flow.js.map", + "packages/contracts/dist/traffic-flow.d.ts", + "packages/contracts/dist/traffic-flow.d.ts.map", + "packages/contracts/dist/uptime-probes.js", + "packages/contracts/dist/uptime-probes.js.map", + "packages/contracts/dist/uptime-probes.d.ts", + "packages/contracts/dist/uptime-probes.d.ts.map", + "packages/contracts/dist/uptime-speed.js", + "packages/contracts/dist/uptime-speed.js.map", + "packages/contracts/dist/uptime-speed.d.ts", + "packages/contracts/dist/uptime-speed.d.ts.map", + "packages/contracts/dist/users.js", + "packages/contracts/dist/users.js.map", + "packages/contracts/dist/users.d.ts", + "packages/contracts/dist/users.d.ts.map", + "packages/contracts/dist/wireguard.js", + "packages/contracts/dist/wireguard.js.map", + "packages/contracts/dist/wireguard.d.ts", + "packages/contracts/dist/wireguard.d.ts.map" + ], + "missing": [], + "different": [] +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/wireguard-ipv6-initial-api-2026-10-11.json b/docs/ux-ui/acceptance/wireguard-ipv6-initial-api-2026-10-11.json new file mode 100644 index 00000000..a374f9cd --- /dev/null +++ b/docs/ux-ui/acceptance/wireguard-ipv6-initial-api-2026-10-11.json @@ -0,0 +1,18 @@ +{ + "date": "2026-10-11", + "block": "12.5.152", + "name": "mm-wg152-1791730354586", + "cases": [ + { + "method": "POST", + "path": "/wireguard/interfaces", + "status": 201 + }, + { + "method": "POST", + "path": "/wireguard/interfaces", + "status": 201 + } + ], + "pings": [] +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/wireguard-ipv6-initial-cleanup-2026-10-11.json b/docs/ux-ui/acceptance/wireguard-ipv6-initial-cleanup-2026-10-11.json new file mode 100644 index 00000000..255e0108 --- /dev/null +++ b/docs/ux-ui/acceptance/wireguard-ipv6-initial-cleanup-2026-10-11.json @@ -0,0 +1,49 @@ +{ + "date": "2026-10-11", + "block": "12.5.152", + "name": "mm-wg152-1791730354586", + "devices": [ + { + "serverId": 1, + "host": "194.226.121.150", + "version": "7.24.5 (stable)", + "reachable": true + }, + { + "serverId": 2, + "host": "104.165.244.7", + "version": "7.24.5 (stable)", + "reachable": true + } + ], + "cleanup": [ + { + "serverId": 1, + "/interface/wireguard/peers": true, + "/interface/wireguard": true, + "/ip/address": true, + "/ipv6/address": true, + "/ip/route": true, + "/ipv6/route": true, + "/ip/firewall/filter": true, + "/ip/firewall/nat": true, + "/ipv6/firewall/filter": true + }, + { + "serverId": 2, + "/interface/wireguard/peers": true, + "/interface/wireguard": true, + "/ip/address": true, + "/ipv6/address": true, + "/ip/route": true, + "/ipv6/route": true, + "/ip/firewall/filter": true, + "/ip/firewall/nat": true, + "/ipv6/firewall/filter": true + } + ], + "childExit": 1, + "error": "Child failed: inspect private log\n\n1 !== 0\n", + "databaseRemoved": true, + "passed": false +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/wireguard-ipv6-native-2026-10-11.json b/docs/ux-ui/acceptance/wireguard-ipv6-native-2026-10-11.json new file mode 100644 index 00000000..fe49c100 --- /dev/null +++ b/docs/ux-ui/acceptance/wireguard-ipv6-native-2026-10-11.json @@ -0,0 +1,48 @@ +{ + "date": "2026-10-11", + "block": "12.5.152", + "name": "mm-wg152-1791731017548", + "devices": [ + { + "serverId": 1, + "host": "194.226.121.150", + "version": "7.24.5 (stable)", + "reachable": true + }, + { + "serverId": 2, + "host": "104.165.244.7", + "version": "7.24.5 (stable)", + "reachable": true + } + ], + "cleanup": [ + { + "serverId": 1, + "/interface/wireguard/peers": true, + "/interface/wireguard": true, + "/ip/address": true, + "/ipv6/address": true, + "/ip/route": true, + "/ipv6/route": true, + "/ip/firewall/filter": true, + "/ip/firewall/nat": true, + "/ipv6/firewall/filter": true + }, + { + "serverId": 2, + "/interface/wireguard/peers": true, + "/interface/wireguard": true, + "/ip/address": true, + "/ipv6/address": true, + "/ip/route": true, + "/ipv6/route": true, + "/ip/firewall/filter": true, + "/ip/firewall/nat": true, + "/ipv6/firewall/filter": true + } + ], + "childExit": 0, + "databaseRemoved": true, + "passed": true +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/wireguard-ipv6-smoke-2026-10-11.json b/docs/ux-ui/acceptance/wireguard-ipv6-smoke-2026-10-11.json new file mode 100644 index 00000000..6231f921 --- /dev/null +++ b/docs/ux-ui/acceptance/wireguard-ipv6-smoke-2026-10-11.json @@ -0,0 +1,49 @@ +{ + "date": "2026-10-11", + "block": "12.5.152", + "runtime": { + "backend": "ux-wg152", + "frontend": "ux-wg152" + }, + "cases": [ + { + "width": 1440, + "status": 200, + "api": [ + { + "path": "/api/servers", + "status": 200 + }, + { + "path": "/api/wireguard", + "status": 200 + } + ], + "pageErrors": [], + "noOverflow": true, + "ipv6AddressHint": true + }, + { + "width": 320, + "status": 200, + "api": [ + { + "path": "/api/servers", + "status": 200 + }, + { + "path": "/api/wireguard", + "status": 200 + } + ], + "pageErrors": [], + "noOverflow": true, + "ipv6AddressHint": true + } + ], + "bounds": [ + "Read-only live catalog/new-form UI smoke after native cleanup; no deployed UI mutation", + "Native real RouterOS mutations/history/SQL accepted via dedicated database with production Fastify handlers", + "IPv6 ULA inner over IPv4 WireGuard outer; no IPv6 outer/independent WAN/throughput/scale claim" + ] +} diff --git a/packages/contracts/src/wireguard.ts b/packages/contracts/src/wireguard.ts index 6ad6f456..cc11e909 100644 --- a/packages/contracts/src/wireguard.ts +++ b/packages/contracts/src/wireguard.ts @@ -12,6 +12,7 @@ export function resolveWgClientEndpoint(clientEndpoint: string | undefined, serv } export const wgStatusSchema = z.enum(["up", "down"]) +export const wgInterfaceAddressSchema = z.union([z.literal(""), z.cidrv4(), z.cidrv6()]) export const wgPeerDtoSchema = z.object({ id: z.string().min(1), @@ -85,12 +86,13 @@ export const wgCreateInterfaceSchema = z.object({ mtu: z.number().int().positive().default(1420), comment: z.string().optional(), privateKey: z.string().min(1).optional(), - address: z.string().optional(), + address: wgInterfaceAddressSchema.optional(), disabled: z.boolean().optional(), peer: wgCreatePeerSchema.optional(), }) export const wgPatchInterfaceSchema = z.object({ + address: wgInterfaceAddressSchema.optional(), name: z.string().min(1).max(64).optional(), listenPort: z.number().int().positive().optional(), mtu: z.number().int().positive().optional(), diff --git a/tests/e2e/wireguard-ipv6.spec.ts b/tests/e2e/wireguard-ipv6.spec.ts new file mode 100644 index 00000000..2a8172a1 --- /dev/null +++ b/tests/e2e/wireguard-ipv6.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from "@playwright/test" +for (const width of [1440, 320]) { + test(`WireGuard IPv6: форма сохраняет адрес после ошибки API, ширина ${width}`, async ({ page }) => { + await page.setViewportSize({ width, height: 1000 }) + let attempts = 0 + const errors: string[] = [] + page.on("pageerror", e => errors.push(e.message)) + const iface = { id: "1:*152", rosId: "*152", name: "wg-ipv6", serverId: "1", serverName: "CHR", listenPort: 13231, mtu: 1420, address: "fd00:152::1/64", comment: "", enabled: true, status: "up", peers: [] } + await page.route("**/health", route => route.fulfill({ json: { ok: true } })) + await page.route("**/api/**", route => { + const path = new URL(route.request().url()).pathname + if (path === "/api/wireguard/interfaces" && route.request().method() === "POST") { + expect(route.request().postDataJSON()).toMatchObject({ name: "wg-ipv6", address: "fd00:152::1/64", serverId: "1" }) + attempts++ + return attempts === 1 ? route.fulfill({ status: 403, json: { error: "Запись запрещена тест" } }) : route.fulfill({ status: 201, json: iface }) + } + return route.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [{ id: 1, name: "CHR", host: "192.0.2.1", enabled: true, type: "exit-node", country: "RU", status: "online" }] : path === "/api/wireguard" ? { interfaces: attempts > 1 ? [iface] : [] } : {} }) + }) + await page.goto("/wireguard") + await page.getByRole("button", { name: "Новый интерфейс", exact: true }).first().click() + const dialog = page.getByRole("dialog") + await dialog.getByLabel("Сервер", { exact: true }).selectOption("1") + await dialog.getByLabel("Имя интерфейса", { exact: true }).fill("wg-ipv6") + await dialog.getByRole("button", { name: "Дополнительно", exact: true }).click() + await dialog.getByLabel("IP на интерфейсе", { exact: true }).fill("fd00:152::1/64") + await expect(dialog.getByLabel("IP на интерфейсе", { exact: true })).toHaveAttribute("placeholder", "IPv4/IPv6 с префиксом") + await dialog.getByRole("button", { name: "Создать туннель", exact: true }).click() + await expect(dialog.getByRole("alert")).toContainText("Запись запрещена тест") + await expect(dialog.getByLabel("IP на интерфейсе", { exact: true })).toHaveValue("fd00:152::1/64") + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await dialog.getByRole("button", { name: "Создать туннель", exact: true }).click() + await expect(dialog).toHaveCount(0) + expect(attempts).toBe(2) + expect(errors).toEqual([]) + }) +}