fix(ipsec): подтвердить достоверность операций и приёмку раздела
This commit is contained in:
1 parent
e2d81f9657
commit
f80d9b7637
28 files changed
+969
-170
No files matched your search
@@ -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 сертификата). */
|
||||
|
||||
Reference in new issue
Block a user