fix(ipsec): клиенты через общий IKEv2-peer и отдельный клиентский сертификат
Docker images / prepare-release (push) Successful in 7s
Docker images / backend-test (push) Successful in 1m51s
Docker images / frontend-image (push) Successful in 2m54s
Docker images / updater-image (push) Successful in 40s
Docker images / backend-image (push) Successful in 2m11s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 10s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
DenozordecandCursor committed 2026-09-12 22:06:35 +07:00
1 parent 11ae7593f4
commit 0e1524c600
9 files changed
+347 -96

No files matched your search

+105 -41
View File
@@ -22,6 +22,7 @@ import {
resolveIke2CaName,
resolveIke2ServerCert,
selectIke2Peers,
selectSharedIke2Identity,
} from "./ipsec-config.js"
import { certificateRole, type CertificateRoleContext } from "./certificate-parse.js"
import {
@@ -210,57 +211,120 @@ export async function fetchIpsecState(server: ServerRow): Promise<IpsecServerSta
return { server, client, peers, identities, modeConfigs, pools, policies, nat, active, certs }
}
/** Клиенты = identity, участвующие в IKEv2 remote-access (managed + существующие RouterOS). */
/**
* Клиенты IKEv2:
* 1) клиентские сертификаты (роль client) — включая существующие client1/anakondra на устройстве;
* 2) PSK-identity.
* Общая listener-identity (без remote-certificate) клиентом не считается — она обслуживает всех.
*/
export function mapClients(state: IpsecServerState): IpsecClientDto[] {
const server = state.server
const ike2PeerNames = selectIke2Peers(state.peers).map((p) => (p.name ?? "").trim()).filter(Boolean)
const ike2Peers = selectIke2Peers(state.peers)
const ike2PeerNames = ike2Peers.map((p) => (p.name ?? "").trim()).filter(Boolean)
const mcByName = new Map(state.modeConfigs.map((m) => [(m.name ?? "").trim(), m]))
const serverName = String(server.name ?? "").trim() || String(server.host ?? server.id)
const roleCtx: CertificateRoleContext = {
peerCertNames: ike2Peers.map((p) => (p.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 shared = selectSharedIke2Identity(state.identities, ike2PeerNames)
const sharedRosId = shared?.[".id"] ? String(shared[".id"]) : undefined
const activeByRemote = new Map<string, RosIpsecActivePeer>()
for (const a of state.active) {
const rid = String(a["remote-id"] ?? "").trim()
if (rid) activeByRemote.set(rid, a)
}
return state.identities
.filter((i) => isIpsecManagedComment(i.comment) || isIke2RemoteAccessIdentity(i, ike2PeerNames))
.map((i): IpsecClientDto => {
const comment = i.comment ?? ""
const managed = isIpsecManagedComment(comment)
const name = identityDisplayName(i, state.certs)
const psk = (i["auth-method"] ?? "") === "pre-shared-key"
const certName = (i["remote-certificate"] ?? "").trim()
const cn = certName
? String(state.certs.find((c) => String(c.name ?? "") === certName)?.["common-name"] ?? "")
: ""
const mcName = (i["mode-config"] ?? "").trim()
const mc = mcByName.get(mcName)
const staticIp = mc
? ((mc.address ?? mc["address-prefix"] ?? "").replace(/\/\d+$/, "").trim() || undefined)
: undefined
const active = (cn ? activeByRemote.get(cn) : undefined)
?? (i["remote-id"] ? activeByRemote.get(i["remote-id"]) : undefined)
return {
id: `${server.id}:${String(i[".id"] ?? "identity")}`,
rosId: String(i[".id"] ?? "identity"),
serverId: String(server.id),
serverName: String(server.name ?? "").trim() || String(server.host ?? server.id),
name,
authMethod: psk ? "pre-shared-key" : "certificate",
certificateName: certName || undefined,
commonName: cn || undefined,
remoteId: (i["remote-id"] ?? "").trim() || undefined,
staticIp,
modeConfigName: mcName || undefined,
peerName: (i.peer ?? "").trim() || undefined,
online: Boolean(active),
activeAddress: active?.address || undefined,
activeSince: active?.established || undefined,
disabled: asBool(i.disabled),
comment: comment || undefined,
managed,
}
const identityByRemoteCert = new Map<string, RosIpsecIdentity>()
for (const i of state.identities) {
const rc = (i["remote-certificate"] ?? "").trim()
if (rc) identityByRemoteCert.set(rc, i)
}
const personalStaticIp = (mcNameRaw: string): string | undefined => {
const mc = mcByName.get(mcNameRaw)
if (!mc) return undefined
return (mc.address ?? mc["address-prefix"] ?? "").replace(/\/\d+$/, "").trim() || undefined
}
const out: IpsecClientDto[] = []
// 1. Клиентские сертификаты (роль client), подписанные нашим CA / выданные менеджером / referenced identity.
for (const cert of state.certs) {
const certName = String(cert.name ?? "").trim()
if (!certName) continue
if (certificateRole(cert, roleCtx) !== "client") continue
const signedBy = (cert.ca ?? "").trim()
const isUser = certName.startsWith("ipsec-user-")
const referenced = identityByRemoteCert.has(certName)
if (!isUser && !referenced && !(caName && signedBy === caName)) continue
const identity = identityByRemoteCert.get(certName)
const comment = identity?.comment ?? ""
const mcName = (identity?.["mode-config"] ?? "").trim()
const cn = String(cert["common-name"] ?? "").trim()
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),
serverId: String(server.id),
serverName,
name: cn || certName,
authMethod: "certificate",
kind: "cert",
certificateName: certName,
certName,
signedBy: signedBy || undefined,
commonName: cn || undefined,
remoteId: (identity?.["remote-id"] ?? "").trim() || undefined,
staticIp: personalStaticIp(mcName),
modeConfigName: mcName || undefined,
peerName: (identity?.peer ?? "").trim() || undefined,
online: Boolean(active),
activeAddress: active?.address || undefined,
activeSince: active?.established || undefined,
disabled: asBool(identity?.disabled),
comment: comment || undefined,
managed: isUser || isIpsecManagedComment(comment),
})
.sort((a, b) => a.name.localeCompare(b.name))
}
// 2. PSK-identity (managed или IKEv2 remote-access).
for (const i of state.identities) {
if ((i["auth-method"] ?? "").trim().toLowerCase() !== "pre-shared-key") continue
const rosId = String(i[".id"] ?? "")
if (sharedRosId && rosId === sharedRosId) continue
const managed = isIpsecManagedComment(i.comment)
if (!managed && !isIke2RemoteAccessIdentity(i, ike2PeerNames)) continue
const mcName = (i["mode-config"] ?? "").trim()
const remoteId = (i["remote-id"] ?? "").trim()
const active = remoteId ? activeByRemote.get(remoteId) : undefined
out.push({
id: `${server.id}:${rosId || "identity"}`,
rosId: rosId || "identity",
serverId: String(server.id),
serverName,
name: identityDisplayName(i, state.certs),
authMethod: "pre-shared-key",
kind: "psk",
remoteId: remoteId || undefined,
staticIp: personalStaticIp(mcName),
modeConfigName: mcName || undefined,
peerName: (i.peer ?? "").trim() || undefined,
online: Boolean(active),
activeAddress: active?.address || undefined,
activeSince: active?.established || undefined,
disabled: asBool(i.disabled),
comment: i.comment || undefined,
managed,
})
}
return out.sort((a, b) => a.name.localeCompare(b.name))
}
export function mapServerSummary(state: IpsecServerState, clients: IpsecClientDto[]): IpsecServerSummaryDto {