fix(ipsec): подтвердить достоверность операций и приёмку раздела

This commit is contained in:
Denozordec committed 2026-10-09 12:46:22 +07:00
1 parent e2d81f9657
commit f80d9b7637
28 files changed
+969 -170

No files matched your search

+52 -33
View File
@@ -1,4 +1,5 @@
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
import { parseDbServerId } from "../utils/server-id.js"
import { eq } from "drizzle-orm"
import { db } from "../db/index.js"
import { servers } from "../db/schema.js"
@@ -20,6 +21,7 @@ import {
identityDisplayName,
isIke2RemoteAccessIdentity,
isIpsecManagedComment,
isManagedIpsecModeConfig,
resolveIke2CaName,
resolveIke2ServerCert,
selectIke2Peers,
@@ -196,18 +198,19 @@ export interface IpsecServerState {
certs: RosCertRow[]
}
export async function fetchIpsecState(server: ServerRow): Promise<IpsecServerState> {
const client = MikrotikClient.fromServer(server)
const empty = <T>(v: unknown): T[] => (Array.isArray(v) ? (v as T[]) : [])
export async function fetchIpsecState(server: ServerRow, reader?: MikrotikClient): Promise<IpsecServerState> {
if (!server.enabled) throw new Error("Сервер выключен. Опрос IPsec не выполнялся")
const client = reader ?? MikrotikClient.fromServer(server)
const collection = async <T>(path: string): Promise<T[]> => {
const raw = await client.get<unknown>(path)
if (!Array.isArray(raw) || raw.some(row => !row || typeof row !== "object" || Array.isArray(row) || typeof row[".id"] !== "string" || !row[".id"])) throw new Error(`Некорректный ответ IPsec: ${path}`)
return raw as T[]
}
const [peers, identities, modeConfigs, pools, policies, nat, active, certs] = await Promise.all([
client.get<unknown>("/ip/ipsec/peer").then((v) => empty<RosIpsecPeer>(v)).catch(() => [] as RosIpsecPeer[]),
client.get<unknown>("/ip/ipsec/identity").then((v) => empty<RosIpsecIdentity>(v)).catch(() => [] as RosIpsecIdentity[]),
client.get<unknown>("/ip/ipsec/mode-config").then((v) => empty<RosIpsecModeConfig>(v)).catch(() => [] as RosIpsecModeConfig[]),
client.get<unknown>("/ip/pool").then((v) => empty<RosIpsecPool>(v)).catch(() => [] as RosIpsecPool[]),
client.get<unknown>("/ip/ipsec/policy").then((v) => empty<RosIpsecPolicy>(v)).catch(() => [] as RosIpsecPolicy[]),
client.get<unknown>("/ip/firewall/nat").then((v) => empty<RosFirewallNat>(v)).catch(() => [] as RosFirewallNat[]),
client.get<unknown>("/ip/ipsec/active-peers").then((v) => empty<RosIpsecActivePeer>(v)).catch(() => [] as RosIpsecActivePeer[]),
client.getCertificates().catch(() => [] as RosCertRow[]),
collection<RosIpsecPeer>("/ip/ipsec/peer"), collection<RosIpsecIdentity>("/ip/ipsec/identity"),
collection<RosIpsecModeConfig>("/ip/ipsec/mode-config"), collection<RosIpsecPool>("/ip/pool"),
collection<RosIpsecPolicy>("/ip/ipsec/policy"), collection<RosFirewallNat>("/ip/firewall/nat"),
collection<RosIpsecActivePeer>("/ip/ipsec/active-peers"), collection<RosCertRow>("/certificate"),
])
return { server, client, peers, identities, modeConfigs, pools, policies, nat, active, certs }
}
@@ -226,10 +229,10 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
const serverName = String(server.name ?? "").trim() || String(server.host ?? server.id)
const roleCtx: CertificateRoleContext = {
peerCertNames: ike2Peers.map((p) => (p.certificate ?? "").trim()).filter(Boolean),
peerCertNames: [...ike2Peers.map((p) => (p.certificate ?? "").trim()), ...state.identities.filter(i => ike2PeerNames.includes((i.peer ?? "").trim())).map(i => (i.certificate ?? "").trim())].filter(Boolean),
identityCertNames: state.identities.map((i) => (i["remote-certificate"] ?? "").trim()).filter(Boolean),
}
const caName = resolveIke2CaName(state.certs, resolveIke2ServerCert(state.certs, ike2Peers))
const caName = resolveIke2CaName(state.certs, resolveIke2ServerCert(state.certs, ike2Peers, state.identities))
const shared = selectSharedIke2Identity(state.identities, ike2PeerNames)
const sharedRosId = shared?.[".id"] ? String(shared[".id"]) : undefined
@@ -270,11 +273,11 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
const active = (cn ? activeByRemote.get(cn) : undefined)
?? (identity?.["remote-id"] ? activeByRemote.get(String(identity["remote-id"]).trim()) : undefined)
out.push({
id: `${server.id}:${String(identity?.[".id"] ?? cert[".id"] ?? certName)}`,
rosId: String(identity?.[".id"] ?? cert[".id"] ?? certName),
id: `${server.id}:${identity ? "identity" : "cert"}:${String(identity?.[".id"] ?? cert[".id"] ?? certName)}`,
rosId: `${identity ? "identity" : "cert"}:${String(identity?.[".id"] ?? cert[".id"] ?? certName)}`,
serverId: String(server.id),
serverName,
name: cn || certName,
name: identity ? identityDisplayName(identity, state.certs) : (cn || certName),
authMethod: "certificate",
kind: "cert",
certificateName: certName,
@@ -305,8 +308,8 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
const remoteId = (i["remote-id"] ?? "").trim()
const active = remoteId ? activeByRemote.get(remoteId) : undefined
out.push({
id: `${server.id}:${rosId || "identity"}`,
rosId: rosId || "identity",
id: `${server.id}:identity:${rosId}`,
rosId: `identity:${rosId}`,
serverId: String(server.id),
serverName,
name: identityDisplayName(i, state.certs),
@@ -328,6 +331,16 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
return out.sort((a, b) => a.name.localeCompare(b.name))
}
/** Explicit namespaces prevent cross-menu RouterOS ID collisions. Legacy ambiguous IDs fail closed. */
export function resolveIpsecClientTarget(state: IpsecServerState, token: string) {
const kind = token.startsWith("cert:") ? "cert" : token.startsWith("identity:") ? "identity" : undefined
const raw = kind ? token.slice(kind.length + 1) : token
const clients = mapClients(state)
const identity = kind === "cert" ? undefined : state.identities.find(i => i[".id"] === raw && clients.some(c => c.rosId === "identity:" + raw))
const certRow = kind === "identity" ? undefined : state.certs.find(c => (c[".id"] === raw || c.name === raw) && clients.some(user => user.kind === "cert" && user.certName === c.name))
return { identity, certRow, ambiguous: Boolean(!kind && identity && certRow) }
}
export function mapServerSummary(state: IpsecServerState, clients: IpsecClientDto[]): IpsecServerSummaryDto {
const server = state.server
const ike2Peers = selectIke2Peers(state.peers)
@@ -353,12 +366,12 @@ export function mapServerSummary(state: IpsecServerState, clients: IpsecClientDt
?? state.pools.find((p) => isIpsecManagedComment(p.comment))
: undefined
const serverCertRow = resolveIke2ServerCert(state.certs, ike2Peers)
const serverCertRow = resolveIke2ServerCert(state.certs, ike2Peers, state.identities)
const caName = resolveIke2CaName(state.certs, serverCertRow)
const caCertRow = caName ? state.certs.find((c) => String(c.name ?? "").trim() === caName) : undefined
const roleCtx: CertificateRoleContext = {
peerCertNames: ike2Peers.map((p) => (p.certificate ?? "").trim()).filter(Boolean),
peerCertNames: [...ike2Peers.map((p) => (p.certificate ?? "").trim()), ...state.identities.filter(i => ike2PeerNames.includes((i.peer ?? "").trim())).map(i => (i.certificate ?? "").trim())].filter(Boolean),
identityCertNames: ike2Identities.map((i) => (i["remote-certificate"] ?? "").trim()).filter(Boolean),
}
const referencedCertNames = new Set<string>([
@@ -432,7 +445,7 @@ export async function captureIpsecSnapshot(server: ServerRow): Promise<IpsecSnap
disabled: asBool(i.disabled),
})),
modeConfigs: state.modeConfigs
.filter((m) => isIpsecManagedComment(m.comment))
.filter(isManagedIpsecModeConfig)
.map((m) => ({
name: (m.name ?? "").trim(),
addressPool: (m["address-pool"] ?? "").trim(),
@@ -496,7 +509,7 @@ export async function fetchIpsecRestoreState(server: ServerRow): Promise<{
.map((i) => ({
rosId: String(i[".id"] ?? ""),
peerName: (i.peer ?? "").trim(),
authMethod: (i["auth-method"] ?? "") === "pre-shared-key" ? "pre-shared-key" as const : "rsa-key" as const,
authMethod: (i["auth-method"] ?? "").trim(),
certificate: (i.certificate ?? "").trim(),
remoteCertificate: (i["remote-certificate"] ?? "").trim(),
matchBy: (i["match-by"] ?? "").trim(),
@@ -508,7 +521,7 @@ export async function fetchIpsecRestoreState(server: ServerRow): Promise<{
disabled: asBool(i.disabled),
})),
modeConfigs: state.modeConfigs
.filter((m) => isIpsecManagedComment(m.comment))
.filter(isManagedIpsecModeConfig)
.map((m) => ({
rosId: String(m[".id"] ?? ""),
name: (m.name ?? "").trim(),
@@ -540,17 +553,21 @@ export async function fetchIpsecRestoreState(server: ServerRow): Promise<{
export type IpsecListResult = IpsecListResponse
export async function listIpsec(opts?: { serverId?: string }): Promise<IpsecListResult> {
export async function listIpsec(opts?: { serverId?: string }, dependencies = {
findServer: async (id: number) => (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0],
enabledServers: async () => db.select().from(servers).where(eq(servers.enabled, true)),
readState: fetchIpsecState,
}): Promise<IpsecListResult> {
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) {
return { servers: [], clients: [], 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 dependencies.findServer(id)
serverRows = row ? [row] : []
} else {
serverRows = await db.select().from(servers).where(eq(servers.enabled, true))
serverRows = await dependencies.enabledServers()
}
const failures: IpsecListResult["failures"] = []
@@ -559,7 +576,8 @@ export async function listIpsec(opts?: { serverId?: string }): Promise<IpsecList
await Promise.all(
serverRows.map(async (server) => {
try {
const state = await fetchIpsecState(server)
if (!server.enabled) throw new Error("Сервер выключен. Опрос IPsec не выполнялся")
const state = await dependencies.readState(server)
const serverClients = mapClients(state)
summaries.push(mapServerSummary(state, serverClients))
clients.push(...serverClients)
@@ -579,10 +597,11 @@ export async function listIpsec(opts?: { serverId?: string }): Promise<IpsecList
export async function countIpsecClients():Promise<number|undefined>{return boundedCompleteCount(()=>listIpsec(),value=>completeCollectionCount(value,"clients"))}
export async function getEnabledIpsecServerById(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
export async function getEnabledIpsecServerById(serverId: string | number, lookup = async (id: number) => (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0]) {
const id = parseDbServerId(serverId)
if (id === null) return null
const row = await lookup(id)
return row?.enabled ? row : null
}
/** Каталог клиентов для модуля «Пользователи» (привязка app-пользователей по CN сертификата). */