feat(ipsec): управление IKEv2/IPsec VPN и клиентами из одного окна
Docker images / prepare-release (push) Successful in 15s
Docker images / backend-test (push) Successful in 2m32s
Docker images / frontend-image (push) Successful in 4m19s
Docker images / updater-image (push) Successful in 50s
Docker images / backend-image (push) Successful in 2m40s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 8s
Docker images / prepare-release (push) Successful in 15s
Docker images / backend-test (push) Successful in 2m32s
Docker images / frontend-image (push) Successful in 4m19s
Docker images / updater-image (push) Successful in 50s
Docker images / backend-image (push) Successful in 2m40s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 8s
- мастер инициализации сервера: CA и серверный сертификаты, peer/profile/proposal, пул, mode-config, policy-template, managed NAT masquerade - клиенты по сертификату (RSA) и PSK: статический IP или из пула, онлайн-статус по active-peers - скачивание .p12 и strongSwan .sswan с инструкцией, перекачка с новой passphrase - история изменений (config_revisions, секция ipsec) и restore только managed-объектов - привязка IPsec-клиентов к пользователям приложения по Common Name - страница /ipsec с KPI и вкладками Клиенты/Сервер/CLI, сайдбар, command palette
This commit is contained in:
1 parent
564aae21f0
commit
7755d77340
35 files changed
+4154
-13
No files matched your search
@@ -0,0 +1,513 @@
|
||||
import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
import type {
|
||||
IpsecCertInfoDto,
|
||||
IpsecClientDto,
|
||||
IpsecListResponse,
|
||||
IpsecModeConfigDto,
|
||||
IpsecPeerDto,
|
||||
IpsecPoolDto,
|
||||
IpsecServerSummaryDto,
|
||||
} from "@mmapp/contracts/ipsec"
|
||||
import {
|
||||
IPSEC_CA_CERT,
|
||||
IPSEC_COMMON_NAME,
|
||||
IPSEC_SERVER_CERT,
|
||||
clientCertName,
|
||||
isIpsecManagedComment,
|
||||
parseIpsecUserComment,
|
||||
} from "./ipsec-config.js"
|
||||
import {
|
||||
canonicalIpsecSnapshot,
|
||||
type IpsecLiveIdentity,
|
||||
type IpsecLiveModeConfig,
|
||||
type IpsecLiveNat,
|
||||
type IpsecLivePeer,
|
||||
type IpsecLivePool,
|
||||
type IpsecSnapshot,
|
||||
} from "./entity-snapshots.js"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
|
||||
export interface RosIpsecPeer {
|
||||
".id"?: string
|
||||
name?: string
|
||||
address?: string
|
||||
"exchange-mode"?: string
|
||||
passive?: string
|
||||
certificate?: string
|
||||
profile?: string
|
||||
disabled?: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface RosIpsecIdentity {
|
||||
".id"?: string
|
||||
peer?: string
|
||||
"auth-method"?: string
|
||||
certificate?: string
|
||||
"remote-certificate"?: string
|
||||
"match-by"?: string
|
||||
secret?: string
|
||||
"remote-id"?: string
|
||||
"mode-config"?: string
|
||||
"generate-policy"?: string
|
||||
disabled?: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface RosIpsecModeConfig {
|
||||
".id"?: string
|
||||
name?: string
|
||||
"address-pool"?: string
|
||||
"address-prefix"?: string
|
||||
address?: string
|
||||
"split-dns"?: string
|
||||
"static-dns"?: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface RosIpsecPool {
|
||||
".id"?: string
|
||||
name?: string
|
||||
ranges?: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface RosIpsecPolicy {
|
||||
".id"?: string
|
||||
"src-address"?: string
|
||||
"dst-address"?: string
|
||||
proposal?: string
|
||||
template?: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface RosFirewallNat {
|
||||
".id"?: string
|
||||
chain?: string
|
||||
action?: string
|
||||
"src-address"?: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface RosIpsecActivePeer {
|
||||
".id"?: string
|
||||
address?: string
|
||||
"remote-id"?: string
|
||||
identity?: string
|
||||
established?: string
|
||||
}
|
||||
|
||||
type RosCertRow = Record<string, string | undefined>
|
||||
|
||||
function asBool(v: string | undefined): boolean {
|
||||
return v === "true" || v === "yes"
|
||||
}
|
||||
|
||||
export function mapCertificate(c: RosCertRow): IpsecCertInfoDto {
|
||||
const name = String(c.name ?? "")
|
||||
const keyUsage = String(c["key-usage"] ?? "")
|
||||
const isCa = keyUsage.includes("key-cert-sign")
|
||||
const isUser = name.startsWith("ipsec-user-")
|
||||
return {
|
||||
name,
|
||||
commonName: c["common-name"] || undefined,
|
||||
keySize: c["key-size"] || undefined,
|
||||
fingerprint: c.fingerprint || undefined,
|
||||
expiresAt: c["invalid-after"] || undefined,
|
||||
trusted: asBool(c.trusted),
|
||||
hasPrivateKey: asBool(c["private-key"]),
|
||||
role: isCa ? "ca" : name === IPSEC_SERVER_CERT ? "server" : isUser ? "client" : "other",
|
||||
managed: isCa && name === IPSEC_CA_CERT
|
||||
|| name === IPSEC_SERVER_CERT
|
||||
|| isUser
|
||||
|| isIpsecManagedComment(c.comment),
|
||||
}
|
||||
}
|
||||
|
||||
function mapPeer(server: ServerRow, p: RosIpsecPeer): IpsecPeerDto {
|
||||
return {
|
||||
id: `${server.id}:${String(p[".id"] ?? p.name ?? "peer")}`,
|
||||
rosId: String(p[".id"] ?? p.name ?? "peer"),
|
||||
serverId: String(server.id),
|
||||
serverName: String(server.name ?? "").trim() || String(server.host ?? server.id),
|
||||
name: (p.name ?? "").trim(),
|
||||
address: p.address || undefined,
|
||||
exchangeMode: p["exchange-mode"] || undefined,
|
||||
passive: asBool(p.passive),
|
||||
certificate: p.certificate || undefined,
|
||||
profile: p.profile || undefined,
|
||||
disabled: asBool(p.disabled),
|
||||
comment: p.comment || undefined,
|
||||
managed: isIpsecManagedComment(p.comment),
|
||||
}
|
||||
}
|
||||
|
||||
function mapModeConfig(server: ServerRow, m: RosIpsecModeConfig): IpsecModeConfigDto {
|
||||
return {
|
||||
id: `${server.id}:${String(m[".id"] ?? m.name ?? "mc")}`,
|
||||
rosId: String(m[".id"] ?? m.name ?? "mc"),
|
||||
serverId: String(server.id),
|
||||
name: (m.name ?? "").trim(),
|
||||
addressPool: m["address-pool"] || m["address-prefix"] || undefined,
|
||||
address: m.address || undefined,
|
||||
splitDns: m["split-dns"] || undefined,
|
||||
staticDns: m["static-dns"] || undefined,
|
||||
comment: m.comment || undefined,
|
||||
managed: isIpsecManagedComment(m.comment),
|
||||
}
|
||||
}
|
||||
|
||||
function mapPool(server: ServerRow, p: RosIpsecPool): IpsecPoolDto {
|
||||
return {
|
||||
id: `${server.id}:${String(p[".id"] ?? p.name ?? "pool")}`,
|
||||
rosId: String(p[".id"] ?? p.name ?? "pool"),
|
||||
serverId: String(server.id),
|
||||
name: (p.name ?? "").trim(),
|
||||
ranges: (p.ranges ?? "").trim(),
|
||||
comment: p.comment || undefined,
|
||||
managed: isIpsecManagedComment(p.comment),
|
||||
}
|
||||
}
|
||||
|
||||
export interface IpsecServerState {
|
||||
server: ServerRow
|
||||
client: MikrotikClient
|
||||
peers: RosIpsecPeer[]
|
||||
identities: RosIpsecIdentity[]
|
||||
modeConfigs: RosIpsecModeConfig[]
|
||||
pools: RosIpsecPool[]
|
||||
policies: RosIpsecPolicy[]
|
||||
nat: RosFirewallNat[]
|
||||
active: RosIpsecActivePeer[]
|
||||
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[]) : [])
|
||||
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[]),
|
||||
])
|
||||
return { server, client, peers, identities, modeConfigs, pools, policies, nat, active, certs }
|
||||
}
|
||||
|
||||
/** Клиенты = managed-идентичности (созданы менеджером). */
|
||||
export function mapClients(state: IpsecServerState): IpsecClientDto[] {
|
||||
const server = state.server
|
||||
const certByCommonName = new Map<string, RosCertRow>()
|
||||
for (const c of state.certs) {
|
||||
const cn = String(c["common-name"] ?? "").trim()
|
||||
if (cn) certByCommonName.set(cn, c)
|
||||
}
|
||||
const mcByName = new Map(state.modeConfigs.map((m) => [(m.name ?? "").trim(), m]))
|
||||
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))
|
||||
.map((i): IpsecClientDto => {
|
||||
const comment = i.comment ?? ""
|
||||
const name = parseIpsecUserComment(comment) ?? (comment || "client")
|
||||
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.name ?? "").startsWith("mc-ipsec-")
|
||||
? (mc.address ?? mc["address-prefix"] ?? "").replace(/\/\d+$/, "") || 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: true,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
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)
|
||||
const sharedMc = state.modeConfigs.find(
|
||||
(m) => (m.name ?? "").trim() === IPSEC_COMMON_NAME || (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)
|
||||
const serverCert = state.certs.find((c) => String(c.name ?? "") === IPSEC_SERVER_CERT)
|
||||
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,
|
||||
peer: managedPeer ? mapPeer(server, managedPeer) : undefined,
|
||||
pool: pool ? mapPool(server, pool) : undefined,
|
||||
sharedModeConfig: sharedMc ? mapModeConfig(server, sharedMc) : undefined,
|
||||
caCert: caCert ? mapCertificate(caCert) : undefined,
|
||||
serverCert: serverCert ? mapCertificate(serverCert) : undefined,
|
||||
natRuleManaged,
|
||||
clientsTotal: clients.length,
|
||||
clientsOnline: clients.filter((c) => c.online).length,
|
||||
certs: state.certs
|
||||
.filter((c) => {
|
||||
const n = String(c.name ?? "")
|
||||
return n === IPSEC_CA_CERT || n === IPSEC_SERVER_CERT || n.startsWith("ipsec-user-")
|
||||
})
|
||||
.map(mapCertificate),
|
||||
}
|
||||
}
|
||||
|
||||
export async function captureIpsecSnapshot(server: ServerRow): Promise<IpsecSnapshot> {
|
||||
const state = await fetchIpsecState(server)
|
||||
return canonicalIpsecSnapshot({
|
||||
peers: state.peers
|
||||
.filter((p) => isIpsecManagedComment(p.comment))
|
||||
.map((p) => ({
|
||||
name: (p.name ?? "").trim(),
|
||||
address: (p.address ?? "").trim(),
|
||||
exchangeMode: (p["exchange-mode"] ?? "").trim(),
|
||||
passive: asBool(p.passive),
|
||||
certificate: (p.certificate ?? "").trim(),
|
||||
profile: (p.profile ?? "").trim(),
|
||||
comment: (p.comment ?? "").trim(),
|
||||
disabled: asBool(p.disabled),
|
||||
})),
|
||||
identities: state.identities
|
||||
.filter((i) => isIpsecManagedComment(i.comment))
|
||||
.map((i) => ({
|
||||
peerName: (i.peer ?? "").trim(),
|
||||
authMethod: (i["auth-method"] ?? "").trim(),
|
||||
certificate: (i.certificate ?? "").trim(),
|
||||
remoteCertificate: (i["remote-certificate"] ?? "").trim(),
|
||||
matchBy: (i["match-by"] ?? "").trim(),
|
||||
secret: (i.secret ?? "").trim(),
|
||||
remoteId: (i["remote-id"] ?? "").trim(),
|
||||
modeConfig: (i["mode-config"] ?? "").trim(),
|
||||
generatePolicy: (i["generate-policy"] ?? "").trim(),
|
||||
comment: (i.comment ?? "").trim(),
|
||||
disabled: asBool(i.disabled),
|
||||
})),
|
||||
modeConfigs: state.modeConfigs
|
||||
.filter((m) => isIpsecManagedComment(m.comment))
|
||||
.map((m) => ({
|
||||
name: (m.name ?? "").trim(),
|
||||
addressPool: (m["address-pool"] ?? "").trim(),
|
||||
address: (m.address ?? m["address-prefix"] ?? "").trim(),
|
||||
staticDns: (m["static-dns"] ?? "").trim(),
|
||||
comment: (m.comment ?? "").trim(),
|
||||
})),
|
||||
pools: state.pools
|
||||
.filter((p) => isIpsecManagedComment(p.comment))
|
||||
.map((p) => ({
|
||||
name: (p.name ?? "").trim(),
|
||||
ranges: (p.ranges ?? "").trim(),
|
||||
comment: (p.comment ?? "").trim(),
|
||||
})),
|
||||
policies: state.policies
|
||||
.filter((p) => isIpsecManagedComment(p.comment))
|
||||
.map((p) => ({
|
||||
srcAddress: (p["src-address"] ?? "").trim(),
|
||||
dstAddress: (p["dst-address"] ?? "").trim(),
|
||||
proposal: (p.proposal ?? "").trim(),
|
||||
comment: (p.comment ?? "").trim(),
|
||||
})),
|
||||
nat: state.nat
|
||||
.filter((n) => isIpsecManagedComment(n.comment))
|
||||
.map((n) => ({
|
||||
chain: (n.chain ?? "").trim(),
|
||||
action: (n.action ?? "").trim(),
|
||||
srcAddress: (n["src-address"] ?? "").trim(),
|
||||
comment: (n.comment ?? "").trim(),
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchIpsecRestoreState(server: ServerRow): Promise<{
|
||||
client: MikrotikClient
|
||||
peers: IpsecLivePeer[]
|
||||
identities: IpsecLiveIdentity[]
|
||||
modeConfigs: IpsecLiveModeConfig[]
|
||||
pools: IpsecLivePool[]
|
||||
nat: IpsecLiveNat[]
|
||||
snapshot: IpsecSnapshot
|
||||
}> {
|
||||
const state = await fetchIpsecState(server)
|
||||
return {
|
||||
client: state.client,
|
||||
peers: state.peers
|
||||
.filter((p) => isIpsecManagedComment(p.comment))
|
||||
.map((p) => ({
|
||||
rosId: String(p[".id"] ?? ""),
|
||||
name: (p.name ?? "").trim(),
|
||||
address: (p.address ?? "").trim(),
|
||||
exchangeMode: (p["exchange-mode"] ?? "").trim(),
|
||||
passive: asBool(p.passive),
|
||||
certificate: (p.certificate ?? "").trim(),
|
||||
profile: (p.profile ?? "").trim(),
|
||||
comment: (p.comment ?? "").trim(),
|
||||
disabled: asBool(p.disabled),
|
||||
})),
|
||||
identities: state.identities
|
||||
.filter((i) => isIpsecManagedComment(i.comment))
|
||||
.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,
|
||||
certificate: (i.certificate ?? "").trim(),
|
||||
remoteCertificate: (i["remote-certificate"] ?? "").trim(),
|
||||
matchBy: (i["match-by"] ?? "").trim(),
|
||||
secret: (i.secret ?? "").trim(),
|
||||
remoteId: (i["remote-id"] ?? "").trim(),
|
||||
modeConfig: (i["mode-config"] ?? "").trim(),
|
||||
generatePolicy: (i["generate-policy"] ?? "").trim(),
|
||||
comment: (i.comment ?? "").trim(),
|
||||
disabled: asBool(i.disabled),
|
||||
})),
|
||||
modeConfigs: state.modeConfigs
|
||||
.filter((m) => isIpsecManagedComment(m.comment))
|
||||
.map((m) => ({
|
||||
rosId: String(m[".id"] ?? ""),
|
||||
name: (m.name ?? "").trim(),
|
||||
addressPool: (m["address-pool"] ?? "").trim(),
|
||||
address: (m.address ?? m["address-prefix"] ?? "").trim(),
|
||||
staticDns: (m["static-dns"] ?? "").trim(),
|
||||
comment: (m.comment ?? "").trim(),
|
||||
})),
|
||||
pools: state.pools
|
||||
.filter((p) => isIpsecManagedComment(p.comment))
|
||||
.map((p) => ({
|
||||
rosId: String(p[".id"] ?? ""),
|
||||
name: (p.name ?? "").trim(),
|
||||
ranges: (p.ranges ?? "").trim(),
|
||||
comment: (p.comment ?? "").trim(),
|
||||
})),
|
||||
nat: state.nat
|
||||
.filter((n) => isIpsecManagedComment(n.comment))
|
||||
.map((n) => ({
|
||||
rosId: String(n[".id"] ?? ""),
|
||||
chain: (n.chain ?? "").trim(),
|
||||
action: (n.action ?? "").trim(),
|
||||
srcAddress: (n["src-address"] ?? "").trim(),
|
||||
comment: (n.comment ?? "").trim(),
|
||||
})),
|
||||
snapshot: await captureIpsecSnapshot(server),
|
||||
}
|
||||
}
|
||||
|
||||
export type IpsecListResult = IpsecListResponse
|
||||
|
||||
export async function listIpsec(opts?: { serverId?: string }): Promise<IpsecListResult> {
|
||||
let serverRows: ServerRow[]
|
||||
if (opts?.serverId) {
|
||||
const id = Number.parseInt(String(opts.serverId), 10)
|
||||
if (!Number.isFinite(id)) {
|
||||
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]
|
||||
serverRows = row ? [row] : []
|
||||
} else {
|
||||
serverRows = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
}
|
||||
|
||||
const failures: IpsecListResult["failures"] = []
|
||||
const summaries: IpsecServerSummaryDto[] = []
|
||||
const clients: IpsecClientDto[] = []
|
||||
await Promise.all(
|
||||
serverRows.map(async (server) => {
|
||||
try {
|
||||
const state = await fetchIpsecState(server)
|
||||
const serverClients = mapClients(state)
|
||||
summaries.push(mapServerSummary(state, serverClients))
|
||||
clients.push(...serverClients)
|
||||
} catch (e) {
|
||||
failures.push({
|
||||
serverId: String(server.id),
|
||||
serverName: server.name ?? undefined,
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
summaries.sort((a, b) => a.serverName.localeCompare(b.serverName))
|
||||
clients.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return { servers: summaries, clients, failures }
|
||||
}
|
||||
|
||||
export async function countIpsecClients(): Promise<number> {
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
listIpsec(),
|
||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
||||
])
|
||||
if (!result) return 0
|
||||
return result.clients.length
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/** Каталог клиентов для модуля «Пользователи» (привязка app-пользователей по CN сертификата). */
|
||||
export async function listIpsecClientsForCatalog(serverId: number): Promise<{
|
||||
clients: IpsecClientDto[]
|
||||
error?: string
|
||||
}> {
|
||||
const row = await getEnabledIpsecServerById(serverId)
|
||||
if (!row) return { clients: [], error: "Сервер не найден" }
|
||||
try {
|
||||
const state = await Promise.race([
|
||||
fetchIpsecState(row),
|
||||
new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Таймаут RouterOS")), 5_000)
|
||||
}),
|
||||
])
|
||||
return { clients: mapClients(state) }
|
||||
} catch (e) {
|
||||
return { clients: [], error: e instanceof Error ? e.message : String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
export { clientCertName }
|
||||
Reference in new issue
Block a user