feat(ipsec): add certificate export functionality by name
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 2m41s
Docker images / frontend-image (push) Successful in 2m57s
Docker images / updater-image (push) Successful in 48s
Docker images / backend-image (push) Successful in 2m48s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 13s
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 2m41s
Docker images / frontend-image (push) Successful in 2m57s
Docker images / updater-image (push) Successful in 48s
Docker images / backend-image (push) Successful in 2m48s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 13s
- Implemented a new backend route for exporting existing client certificates in .p12 format by name. - Enhanced the frontend to support exporting certificates directly from the IPsec server grid. - Updated the IPsec page to manage certificate states and handle exports effectively. - Introduced new utility functions for certificate handling and improved data structures to accommodate the changes. - Added tests to ensure the reliability of the new certificate export feature.
This commit is contained in:
1 parent
7365d8d8fb
commit
3eb75ea0b8
13 files changed
+520
-73
No files matched your search
@@ -17,8 +17,13 @@ import {
|
||||
IPSEC_SERVER_CERT,
|
||||
clientCertName,
|
||||
identityDisplayName,
|
||||
isIke2RemoteAccessIdentity,
|
||||
isIpsecManagedComment,
|
||||
resolveIke2CaName,
|
||||
resolveIke2ServerCert,
|
||||
selectIke2Peers,
|
||||
} from "./ipsec-config.js"
|
||||
import { certificateRole, type CertificateRoleContext } from "./certificate-parse.js"
|
||||
import {
|
||||
canonicalIpsecSnapshot,
|
||||
type IpsecLiveIdentity,
|
||||
@@ -107,13 +112,13 @@ function asBool(v: string | undefined): boolean {
|
||||
return v === "true" || v === "yes"
|
||||
}
|
||||
|
||||
export function mapCertificate(c: RosCertRow): IpsecCertInfoDto {
|
||||
export function mapCertificate(c: RosCertRow, ctx: CertificateRoleContext = {}): IpsecCertInfoDto {
|
||||
const name = String(c.name ?? "")
|
||||
const keyUsage = String(c["key-usage"] ?? "")
|
||||
const isCa = keyUsage.includes("key-cert-sign")
|
||||
const isServer = keyUsage.includes("tls-server")
|
||||
const isClient = keyUsage.includes("tls-client")
|
||||
const isUser = name.startsWith("ipsec-user-")
|
||||
const role = certificateRole(c, {
|
||||
peerCertNames: ctx.peerCertNames,
|
||||
identityCertNames: ctx.identityCertNames,
|
||||
})
|
||||
return {
|
||||
name,
|
||||
commonName: c["common-name"] || undefined,
|
||||
@@ -122,8 +127,9 @@ export function mapCertificate(c: RosCertRow): IpsecCertInfoDto {
|
||||
expiresAt: c["invalid-after"] || undefined,
|
||||
trusted: asBool(c.trusted),
|
||||
hasPrivateKey: asBool(c["private-key"]),
|
||||
role: isCa ? "ca" : name === IPSEC_SERVER_CERT || isServer ? "server" : isUser || isClient ? "client" : "other",
|
||||
managed: isCa && name === IPSEC_CA_CERT
|
||||
role,
|
||||
signedBy: (c.ca ?? "").trim() || undefined,
|
||||
managed: (name === IPSEC_CA_CERT && role === "ca")
|
||||
|| name === IPSEC_SERVER_CERT
|
||||
|| isUser
|
||||
|| isIpsecManagedComment(c.comment),
|
||||
@@ -204,9 +210,10 @@ export async function fetchIpsecState(server: ServerRow): Promise<IpsecServerSta
|
||||
return { server, client, peers, identities, modeConfigs, pools, policies, nat, active, certs }
|
||||
}
|
||||
|
||||
/** Клиенты = все identity роутера (managed + существующие RouterOS). */
|
||||
/** Клиенты = identity, участвующие в IKEv2 remote-access (managed + существующие RouterOS). */
|
||||
export function mapClients(state: IpsecServerState): IpsecClientDto[] {
|
||||
const server = state.server
|
||||
const ike2PeerNames = selectIke2Peers(state.peers).map((p) => (p.name ?? "").trim()).filter(Boolean)
|
||||
const mcByName = new Map(state.modeConfigs.map((m) => [(m.name ?? "").trim(), m]))
|
||||
const activeByRemote = new Map<string, RosIpsecActivePeer>()
|
||||
for (const a of state.active) {
|
||||
@@ -215,6 +222,7 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
|
||||
}
|
||||
|
||||
return state.identities
|
||||
.filter((i) => isIpsecManagedComment(i.comment) || isIke2RemoteAccessIdentity(i, ike2PeerNames))
|
||||
.map((i): IpsecClientDto => {
|
||||
const comment = i.comment ?? ""
|
||||
const managed = isIpsecManagedComment(comment)
|
||||
@@ -257,38 +265,70 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
|
||||
|
||||
export function mapServerSummary(state: IpsecServerState, clients: IpsecClientDto[]): IpsecServerSummaryDto {
|
||||
const server = state.server
|
||||
const managedPeer = state.peers.find((p) => isIpsecManagedComment(p.comment))
|
||||
?? state.peers.find((p) => (p.name ?? "").trim() === IPSEC_COMMON_NAME)
|
||||
/** Для отображения: managed peer, иначе первый существующий peer роутера. */
|
||||
const primaryPeer = managedPeer ?? state.peers[0]
|
||||
const sharedMc = state.modeConfigs.find(
|
||||
(m) => (m.name ?? "").trim() === IPSEC_COMMON_NAME || (isIpsecManagedComment(m.comment) && !(m.name ?? "").startsWith("mc-ipsec-")),
|
||||
const ike2Peers = selectIke2Peers(state.peers)
|
||||
const ike2PeerNames = ike2Peers.map((p) => (p.name ?? "").trim()).filter(Boolean)
|
||||
const ike2Identities = state.identities.filter(
|
||||
(i) => isIpsecManagedComment(i.comment) || isIke2RemoteAccessIdentity(i, ike2PeerNames),
|
||||
)
|
||||
|
||||
const managedPeer = ike2Peers.find((p) => isIpsecManagedComment(p.comment))
|
||||
?? ike2Peers.find((p) => (p.name ?? "").trim() === IPSEC_COMMON_NAME)
|
||||
/** Для отображения: managed IKEv2 peer, иначе первый существующий IKEv2 peer. */
|
||||
const primaryPeer = managedPeer ?? ike2Peers[0]
|
||||
|
||||
// mode-config, на который ссылаются IKEv2 identity (не персональный mc-ipsec-*), fallback managed shared
|
||||
const idMcName = ike2Identities
|
||||
.map((i) => (i["mode-config"] ?? "").trim())
|
||||
.find((n) => n && !n.startsWith("mc-ipsec-"))
|
||||
const sharedMc = (idMcName ? state.modeConfigs.find((m) => (m.name ?? "").trim() === idMcName) : undefined)
|
||||
?? state.modeConfigs.find((m) => (m.name ?? "").trim() === IPSEC_COMMON_NAME)
|
||||
?? state.modeConfigs.find((m) => isIpsecManagedComment(m.comment) && !(m.name ?? "").startsWith("mc-ipsec-"))
|
||||
const pool = sharedMc
|
||||
? state.pools.find((p) => (p.name ?? "").trim() === (sharedMc["address-pool"] ?? "").trim())
|
||||
?? state.pools.find((p) => isIpsecManagedComment(p.comment))
|
||||
: undefined
|
||||
const caCert = state.certs.find((c) => String(c.name ?? "") === IPSEC_CA_CERT)
|
||||
?? state.certs.find((c) => String(c["key-usage"] ?? "").includes("key-cert-sign"))
|
||||
const serverCert = state.certs.find((c) => String(c.name ?? "") === IPSEC_SERVER_CERT)
|
||||
?? state.certs.find((c) => String(c["key-usage"] ?? "").includes("tls-server"))
|
||||
|
||||
const serverCertRow = resolveIke2ServerCert(state.certs, ike2Peers)
|
||||
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),
|
||||
identityCertNames: ike2Identities.map((i) => (i["remote-certificate"] ?? "").trim()).filter(Boolean),
|
||||
}
|
||||
const referencedCertNames = new Set<string>([
|
||||
...(roleCtx.peerCertNames ?? []),
|
||||
...(roleCtx.identityCertNames ?? []),
|
||||
...ike2Identities.map((i) => (i.certificate ?? "").trim()).filter(Boolean),
|
||||
])
|
||||
// только IKEv2-релевантные серты: CA сервера, referenced peer/identity и подписанные этим CA
|
||||
const relevantCerts = state.certs.filter((c) => {
|
||||
const n = String(c.name ?? "").trim()
|
||||
if (caName && n === caName) return true
|
||||
if (referencedCertNames.has(n)) return true
|
||||
return Boolean(caName) && (c.ca ?? "").trim() === caName
|
||||
})
|
||||
|
||||
const caCert = caCertRow ? mapCertificate(caCertRow, roleCtx) : undefined
|
||||
const serverCert = serverCertRow ? mapCertificate(serverCertRow, roleCtx) : undefined
|
||||
const natRuleManaged = state.nat.some((r) => isIpsecManagedComment(r.comment) && r.chain === "srcnat")
|
||||
return {
|
||||
serverId: String(server.id),
|
||||
serverName: String(server.name ?? "").trim() || String(server.host ?? server.id),
|
||||
serverCountry: server.country ?? undefined,
|
||||
initialized: Boolean(managedPeer && caCert && serverCert),
|
||||
serverEndpoint: serverCert ? String(serverCert["common-name"] ?? "") || undefined : undefined,
|
||||
ike2Ready: Boolean(ike2Peers.length > 0 && caCert && serverCert),
|
||||
serverEndpoint: serverCertRow ? String(serverCertRow["common-name"] ?? "") || undefined : undefined,
|
||||
peer: primaryPeer ? mapPeer(server, primaryPeer) : undefined,
|
||||
peers: state.peers.map((p) => mapPeer(server, p)),
|
||||
peers: ike2Peers.map((p) => mapPeer(server, p)),
|
||||
pool: pool ? mapPool(server, pool) : undefined,
|
||||
sharedModeConfig: sharedMc ? mapModeConfig(server, sharedMc) : undefined,
|
||||
caCert: caCert ? mapCertificate(caCert) : undefined,
|
||||
serverCert: serverCert ? mapCertificate(serverCert) : undefined,
|
||||
caCert,
|
||||
serverCert,
|
||||
natRuleManaged,
|
||||
clientsTotal: clients.length,
|
||||
clientsOnline: clients.filter((c) => c.online).length,
|
||||
certs: state.certs.map(mapCertificate),
|
||||
certs: relevantCerts.map((c) => mapCertificate(c, roleCtx)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user