Files
MikrotikManager/backend/src/services/ipsec-cleanup.ts
T

35 lines
3.6 KiB
TypeScript

import type { MikrotikClient } from "./mikrotik.js"
import { IPSEC_CA_CERT, IPSEC_SERVER_CERT, IPSEC_POLICY_GROUP, isManagedIpsecModeConfig, isIpsecManagedComment } from "./ipsec-config.js"
import { listByPath } from "./ipsec-ros.js"
/** Read the whole deletion scope before writing; never treat unreadable state as empty. */
export async function removeManagedIpsecServer(client: MikrotikClient, removeCertificates: boolean): Promise<void> {
const paths = ["/ip/ipsec/identity", "/ip/ipsec/peer", "/ip/ipsec/policy", "/ip/ipsec/mode-config", "/ip/pool", "/ip/firewall/nat", "/ip/ipsec/policy/group"]
if (removeCertificates) paths.push("/certificate")
const snapshots = await Promise.all(paths.map(async path => ({ path, rows: await listByPath(client, path) })))
const linkedGroups = new Set(snapshots.find(s => s.path === "/ip/ipsec/policy")?.rows.filter(r => isIpsecManagedComment(r.comment)).map(r => r.group))
const owned = (path: string, row: Record<string, string | undefined>) => path === "/ip/ipsec/policy/group" ? row.name === IPSEC_POLICY_GROUP && linkedGroups.has(row.name) : isIpsecManagedComment(row.comment) || (path === "/ip/ipsec/mode-config" && isManagedIpsecModeConfig(row)) || (path === "/certificate" && (row.name === IPSEC_CA_CERT || row.name === IPSEC_SERVER_CERT || row.name?.startsWith("ipsec-user-")))
const names = (path: string) => new Set(snapshots.find(s => s.path === path)?.rows.filter(row => owned(path, row)).map(row => row.name).filter((name): name is string => typeof name === "string" && name.length > 0))
const peerNames = names("/ip/ipsec/peer"), modeConfigs = names("/ip/ipsec/mode-config"), certificates = names("/certificate"), groups = names("/ip/ipsec/policy/group")
for (const { path, rows } of snapshots) {
for (const row of rows.filter(row => !owned(path, row))) {
if (path === "/certificate" && certificates.has(row.ca ?? "")) throw new Error("Удаление заблокировано: управляемый CA подписал чужой сертификат")
if (path === "/ip/ipsec/identity" && (groups.has(row["policy-template-group"] ?? "") || peerNames.has(row.peer ?? "") || modeConfigs.has(row["mode-config"] ?? "") || certificates.has(row.certificate ?? "") || certificates.has(row["remote-certificate"] ?? ""))) throw new Error("Удаление заблокировано: объекты IPsec используются чужой identity")
if (path === "/ip/ipsec/policy" && row.dynamic !== "true" && row.dynamic !== "yes" && (row.peer ?? "").split(",").some(peer => peerNames.has(peer.trim()))) throw new Error("Удаление заблокировано: peer используется чужой policy")
if (path === "/ip/ipsec/policy" && row.dynamic !== "true" && row.dynamic !== "yes" && groups.has(row.group ?? "")) throw new Error("Удаление заблокировано: группа используется чужой policy")
}
}
for (const { path, rows } of snapshots) {
if (rows.some(row => owned(path, row) && !row[".id"])) throw new Error(`Нет ID управляемого объекта: ${path}`)
}
for (const { path, rows } of snapshots) {
// Default/foreign templates are preserved. Certificates use the existing reserved naming convention.
const targets = rows.filter(row => owned(path, row))
if (path === "/certificate") targets.sort((a, b) => Number(a.name === IPSEC_CA_CERT) - Number(b.name === IPSEC_CA_CERT))
for (const row of targets) {
if (!row[".id"]) throw new Error(`Нет ID управляемого объекта: ${path}`)
await client.delete(`${path}/${encodeURIComponent(row[".id"])}`)
}
}
}