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,498 @@
|
||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
import type { FastifyReply } from "fastify"
|
||||
import {
|
||||
ipsecCertExportRequestSchema,
|
||||
ipsecInitRequestSchema,
|
||||
ipsecUserCreateRequestSchema,
|
||||
ipsecUserPatchSchema,
|
||||
type IpsecCertBundle,
|
||||
type IpsecClientDto,
|
||||
} from "@mmapp/contracts/ipsec"
|
||||
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
|
||||
import {
|
||||
IPSEC_CA_CERT,
|
||||
IPSEC_COMMON_NAME,
|
||||
IPSEC_SERVER_CERT,
|
||||
buildClientInstructions,
|
||||
buildSswanConfig,
|
||||
clientCertName,
|
||||
findFreePoolIp,
|
||||
ipsecManagedComment,
|
||||
ipsecUserComment,
|
||||
isIpsecManagedComment,
|
||||
parseIpsecUserComment,
|
||||
poolRangesFromCidr,
|
||||
userModeConfigName,
|
||||
} from "../services/ipsec-config.js"
|
||||
import {
|
||||
ensureIpsecPeer,
|
||||
ensureIpsecPool,
|
||||
ensureIpsecProfile,
|
||||
ensureIpsecProposal,
|
||||
ensureNatRule,
|
||||
ensurePolicyTemplate,
|
||||
ensureSharedModeConfig,
|
||||
putUserModeConfig,
|
||||
deleteUserModeConfig,
|
||||
putIdentity,
|
||||
patchIdentity,
|
||||
deleteIdentity,
|
||||
listByPath,
|
||||
} from "../services/ipsec-ros.js"
|
||||
import {
|
||||
ensureCaCertificate,
|
||||
ensureServerCertificate,
|
||||
exportClientP12,
|
||||
findCertificate,
|
||||
issueClientCertificate,
|
||||
} from "../services/ipsec-ca.js"
|
||||
import {
|
||||
captureIpsecSnapshot,
|
||||
fetchIpsecRestoreState,
|
||||
fetchIpsecState,
|
||||
getEnabledIpsecServerById,
|
||||
listIpsec,
|
||||
mapClients,
|
||||
mapServerSummary,
|
||||
} from "../services/ipsec-live.js"
|
||||
import {
|
||||
captureAndAppendRevision,
|
||||
listRevisions,
|
||||
loadRevisionForRestore,
|
||||
type ConfigRevisionSource,
|
||||
} from "../services/config-revisions.js"
|
||||
import { parseIpsecSnapshot, planIpsecRestore } from "../services/entity-snapshots.js"
|
||||
import { executeRosOps } from "../services/ros-ops.js"
|
||||
import { parseDbServerId } from "../utils/server-id.js"
|
||||
import { randomBytes } from "node:crypto"
|
||||
import { z } from "zod"
|
||||
|
||||
const IPV4_RE = /^\d{1,3}(?:\.\d{1,3}){3}$/
|
||||
|
||||
function generatePassphrase(): string {
|
||||
return randomBytes(9).toString("base64url")
|
||||
}
|
||||
|
||||
function serverIdParam(v: string): string {
|
||||
return decodeURIComponent(v)
|
||||
}
|
||||
|
||||
function rosIdParam(v: string): string {
|
||||
return decodeURIComponent(v)
|
||||
}
|
||||
|
||||
function errReply(reply: FastifyReply, e: unknown) {
|
||||
const msg = e instanceof MikrotikError ? e.message : e instanceof Error ? e.message : String(e)
|
||||
return reply.status(502).send({ error: `RouterOS: ${msg}` })
|
||||
}
|
||||
|
||||
async function recordIpsec(
|
||||
server: NonNullable<Awaited<ReturnType<typeof getEnabledIpsecServerById>>>,
|
||||
source: ConfigRevisionSource,
|
||||
) {
|
||||
await captureAndAppendRevision({
|
||||
serverId: server.id,
|
||||
section: "ipsec",
|
||||
source,
|
||||
capture: () => captureIpsecSnapshot(server),
|
||||
})
|
||||
}
|
||||
|
||||
/** Существующие статические IP клиентов (персональные mode-config). */
|
||||
function takenStaticIps(modeConfigs: Array<{ name?: string; address?: string; "address-prefix"?: string }>): string[] {
|
||||
return modeConfigs
|
||||
.filter((m) => (m.name ?? "").startsWith("mc-ipsec-"))
|
||||
.map((m) => String(m.address ?? m["address-prefix"] ?? "").replace(/\/\d+$/, "").trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
async function buildCertBundle(
|
||||
client: MikrotikClient,
|
||||
args: { userName: string; serverEndpoint: string; passphrase: string; dns?: string },
|
||||
): Promise<IpsecCertBundle> {
|
||||
const { fileName, content } = await exportClientP12(client, args.userName, args.passphrase)
|
||||
const p12B64 = content.toString("base64")
|
||||
const certName = clientCertName(args.userName)
|
||||
return {
|
||||
user: args.userName,
|
||||
serverEndpoint: args.serverEndpoint,
|
||||
filename: fileName,
|
||||
contentB64: p12B64,
|
||||
mime: "application/x-pkcs12",
|
||||
passphrase: args.passphrase,
|
||||
sswanFilename: `${certName}.sswan`,
|
||||
sswanContent: buildSswanConfig({
|
||||
name: `IKEv2 ${args.serverEndpoint}`,
|
||||
serverEndpoint: args.serverEndpoint,
|
||||
serverId: args.serverEndpoint,
|
||||
p12B64,
|
||||
}),
|
||||
instructions: buildClientInstructions({
|
||||
userName: args.userName,
|
||||
serverEndpoint: args.serverEndpoint,
|
||||
p12Filename: fileName,
|
||||
passphrase: args.passphrase,
|
||||
dns: args.dns,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const RevisionIdParamSchema = z.object({ id: z.string().min(1) })
|
||||
|
||||
const ipsecRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.get("/ipsec", async (req, reply) => {
|
||||
const q = req.query as { serverId?: string }
|
||||
const result = await listIpsec({ serverId: q.serverId })
|
||||
const sid = parseDbServerId(q.serverId)
|
||||
if (sid !== null) {
|
||||
const server = await getEnabledIpsecServerById(sid)
|
||||
if (server) await recordIpsec(server, "observed")
|
||||
}
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.post("/ipsec/server/init", async (req, reply) => {
|
||||
const parsed = ipsecInitRequestSchema.safeParse(req.body ?? {})
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
||||
}
|
||||
const body = parsed.data
|
||||
const ranges = poolRangesFromCidr(body.poolCidr)
|
||||
if (!ranges) {
|
||||
return reply.status(400).send({ error: `Некорректная подсеть пула: ${body.poolCidr}` })
|
||||
}
|
||||
const server = await getEnabledIpsecServerById(body.serverId)
|
||||
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
const caCert = await ensureCaCertificate(client, body.caDaysValid)
|
||||
const serverCert = await ensureServerCertificate(client, {
|
||||
serverEndpoint: body.serverEndpoint,
|
||||
caCertName: caCert,
|
||||
daysValid: body.serverDaysValid,
|
||||
})
|
||||
const comment = ipsecManagedComment("IKEv2 road-warrior")
|
||||
await ensureIpsecProfile(client, IPSEC_COMMON_NAME, comment)
|
||||
await ensureIpsecProposal(client, IPSEC_COMMON_NAME, comment)
|
||||
await ensureIpsecPool(client, IPSEC_COMMON_NAME, ranges, comment)
|
||||
await ensureSharedModeConfig(client, IPSEC_COMMON_NAME, IPSEC_COMMON_NAME, body.dns, comment)
|
||||
await ensurePolicyTemplate(client, body.poolCidr, IPSEC_COMMON_NAME, comment)
|
||||
await ensureIpsecPeer(client, IPSEC_COMMON_NAME, serverCert, IPSEC_COMMON_NAME, comment)
|
||||
if (body.createNatRule) {
|
||||
await ensureNatRule(client, body.poolCidr, ipsecManagedComment("интернет клиентам VPN"))
|
||||
}
|
||||
await recordIpsec(server, "apply")
|
||||
const state = await fetchIpsecState(server)
|
||||
return reply.status(201).send(mapServerSummary(state, mapClients(state)))
|
||||
} catch (e) {
|
||||
return errReply(reply, e)
|
||||
}
|
||||
})
|
||||
|
||||
app.delete("/ipsec/server/:serverId", async (req, reply) => {
|
||||
const { serverId } = req.params as { serverId: string }
|
||||
const q = req.query as { removeCertificates?: string }
|
||||
const removeCertificates = q.removeCertificates !== "false"
|
||||
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
|
||||
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
for (const path of ["/ip/ipsec/identity", "/ip/ipsec/mode-config", "/ip/ipsec/peer", "/ip/ipsec/policy", "/ip/pool"]) {
|
||||
const rows = await listByPath(client, path)
|
||||
for (const row of rows) {
|
||||
if (!row[".id"]) continue
|
||||
if (isIpsecManagedComment(row.comment) || (path === "/ip/ipsec/policy" && (row.template === "true" || row.template === "yes"))) {
|
||||
await client.delete(`${path}/${encodeURIComponent(row[".id"])}`).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
const nat = await listByPath(client, "/ip/firewall/nat")
|
||||
for (const row of nat) {
|
||||
if (row[".id"] && isIpsecManagedComment(row.comment)) {
|
||||
await client.delete(`/ip/firewall/nat/${encodeURIComponent(row[".id"])}`).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
// profile/proposal не критично оставлять; сертификаты — по флагу
|
||||
if (removeCertificates) {
|
||||
const certs = await client.getCertificates()
|
||||
for (const c of certs) {
|
||||
const name = String(c.name ?? "")
|
||||
if (c[".id"] && (name === IPSEC_CA_CERT || name === IPSEC_SERVER_CERT || name.startsWith("ipsec-user-"))) {
|
||||
await client.delete(`/certificate/${encodeURIComponent(c[".id"])}`).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
await recordIpsec(server, "apply")
|
||||
return reply.send({ ok: true })
|
||||
} catch (e) {
|
||||
return errReply(reply, e)
|
||||
}
|
||||
})
|
||||
|
||||
app.post("/ipsec/users", async (req, reply) => {
|
||||
const parsed = ipsecUserCreateRequestSchema.safeParse(req.body ?? {})
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
||||
}
|
||||
const body = parsed.data
|
||||
const server = await getEnabledIpsecServerById(body.serverId)
|
||||
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
const state = await fetchIpsecState(server)
|
||||
const peer = state.peers.find((p) => isIpsecManagedComment(p.comment) || (p.name ?? "").trim() === IPSEC_COMMON_NAME)
|
||||
const sharedMc = state.modeConfigs.find((m) => (m.name ?? "").trim() === IPSEC_COMMON_NAME)
|
||||
const caCert = state.certs.find((c) => String(c.name ?? "") === IPSEC_CA_CERT)
|
||||
const serverCert = state.certs.find((c) => String(c.name ?? "") === IPSEC_SERVER_CERT)
|
||||
if (!peer || !sharedMc || !caCert || !serverCert) {
|
||||
return reply.status(400).send({ error: "IKEv2-сервер не инициализирован — запустите мастер на вкладке «Сервер»" })
|
||||
}
|
||||
const peerName = (peer.name ?? "").trim()
|
||||
const serverEndpoint = String(serverCert["common-name"] ?? "").trim()
|
||||
const dns = sharedMc["static-dns"]?.trim() || undefined
|
||||
|
||||
if (body.authMethod === "pre-shared-key" && !body.psk) {
|
||||
return reply.status(400).send({ error: "Для PSK-клиента укажите secret (psk)" })
|
||||
}
|
||||
|
||||
let modeConfig = (sharedMc.name ?? "").trim()
|
||||
if (body.staticIp) {
|
||||
const ip = body.staticIp.trim()
|
||||
if (!IPV4_RE.test(ip)) return reply.status(400).send({ error: `Некорректный IP: ${ip}` })
|
||||
const taken = takenStaticIps(state.modeConfigs)
|
||||
if (taken.includes(ip)) {
|
||||
return reply.status(409).send({ error: `IP ${ip} уже назначен другому клиенту` })
|
||||
}
|
||||
modeConfig = userModeConfigName(body.name)
|
||||
await putUserModeConfig(client, modeConfig, ip, ipsecManagedComment(`клиент ${body.name.trim()}`))
|
||||
}
|
||||
|
||||
let certName: string | undefined
|
||||
if (body.authMethod === "certificate") {
|
||||
const issued = await issueClientCertificate(client, {
|
||||
userName: body.name,
|
||||
caCertName: IPSEC_CA_CERT,
|
||||
daysValid: body.daysValid ?? 1825,
|
||||
})
|
||||
if (issued.existed) {
|
||||
return reply.status(409).send({ error: `Клиент с сертификатом ${issued.certName} уже существует` })
|
||||
}
|
||||
certName = issued.certName
|
||||
}
|
||||
|
||||
await putIdentity(client, {
|
||||
peerName,
|
||||
modeConfig,
|
||||
comment: ipsecUserComment(body.name),
|
||||
authMethod: body.authMethod,
|
||||
certificate: body.authMethod === "certificate" ? IPSEC_SERVER_CERT : undefined,
|
||||
remoteCertificate: certName,
|
||||
secret: body.psk,
|
||||
remoteId: body.authMethod === "pre-shared-key" ? (body.remoteId ?? body.name) : undefined,
|
||||
})
|
||||
|
||||
let bundle: IpsecCertBundle | undefined
|
||||
if (body.authMethod === "certificate" && certName) {
|
||||
const passphrase = body.passphrase?.trim() || generatePassphrase()
|
||||
bundle = await buildCertBundle(client, {
|
||||
userName: body.name,
|
||||
serverEndpoint,
|
||||
passphrase,
|
||||
dns,
|
||||
})
|
||||
}
|
||||
|
||||
await recordIpsec(server, "apply")
|
||||
const fresh = await fetchIpsecState(server)
|
||||
const clients = mapClients(fresh)
|
||||
const created: IpsecClientDto | undefined = clients.find(
|
||||
(c) => c.name === body.name.trim(),
|
||||
) ?? clients.find((c) => c.certificateName === certName)
|
||||
return reply.status(201).send({ client: created ?? null, bundle })
|
||||
} catch (e) {
|
||||
return errReply(reply, e)
|
||||
}
|
||||
})
|
||||
|
||||
app.patch("/ipsec/users/:serverId/:rosId", async (req, reply) => {
|
||||
const { serverId, rosId } = req.params as { serverId: string; rosId: string }
|
||||
const parsed = ipsecUserPatchSchema.safeParse(req.body ?? {})
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
||||
}
|
||||
const d = parsed.data
|
||||
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
|
||||
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
const state = await fetchIpsecState(server)
|
||||
const identity = state.identities.find((i) => String(i[".id"] ?? "") === rosIdParam(rosId))
|
||||
if (!identity) return reply.status(404).send({ error: "Клиент не найден" })
|
||||
const oldName = parseIpsecUserComment(identity.comment) ?? identity.comment ?? ""
|
||||
const sharedMc = state.modeConfigs.find((m) => (m.name ?? "").trim() === IPSEC_COMMON_NAME)
|
||||
|
||||
if (d.name && d.name !== oldName) {
|
||||
await patchIdentity(client, identity[".id"]!, {
|
||||
comment: ipsecUserComment(d.name),
|
||||
})
|
||||
}
|
||||
|
||||
if (d.staticIp !== undefined) {
|
||||
if (d.staticIp == null) {
|
||||
// вернуть выдачу из пула
|
||||
if (sharedMc) await patchIdentity(client, identity[".id"]!, { "mode-config": (sharedMc.name ?? "").trim() })
|
||||
const personal = (identity["mode-config"] ?? "").trim()
|
||||
if (personal.startsWith("mc-ipsec-")) await deleteUserModeConfig(client, personal)
|
||||
} else {
|
||||
const ip = d.staticIp.trim()
|
||||
if (!IPV4_RE.test(ip)) return reply.status(400).send({ error: `Некорректный IP: ${ip}` })
|
||||
const others = takenStaticIps(
|
||||
state.modeConfigs.filter((m) => (m.name ?? "").trim() !== (identity["mode-config"] ?? "").trim()),
|
||||
)
|
||||
if (others.includes(ip)) return reply.status(409).send({ error: `IP ${ip} уже назначен другому клиенту` })
|
||||
const name = userModeConfigName(d.name || oldName)
|
||||
await putUserModeConfig(client, name, ip, ipsecManagedComment(`клиент ${(d.name || oldName).trim()}`))
|
||||
await patchIdentity(client, identity[".id"]!, { "mode-config": name })
|
||||
}
|
||||
}
|
||||
|
||||
const patch: Record<string, string> = {}
|
||||
if (d.psk) patch.secret = d.psk
|
||||
if (d.remoteId !== undefined) patch["remote-id"] = d.remoteId
|
||||
if (d.disabled === true) patch.disabled = "yes"
|
||||
if (d.disabled === false) patch.disabled = "no"
|
||||
if (Object.keys(patch).length) await patchIdentity(client, identity[".id"]!, patch)
|
||||
|
||||
await recordIpsec(server, "apply")
|
||||
return reply.send({ ok: true })
|
||||
} catch (e) {
|
||||
return errReply(reply, e)
|
||||
}
|
||||
})
|
||||
|
||||
app.delete("/ipsec/users/:serverId/:rosId", async (req, reply) => {
|
||||
const { serverId, rosId } = req.params as { serverId: string; rosId: string }
|
||||
const q = req.query as { removeCertificate?: string }
|
||||
const removeCertificate = q.removeCertificate !== "false"
|
||||
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
|
||||
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
const state = await fetchIpsecState(server)
|
||||
const identity = state.identities.find((i) => String(i[".id"] ?? "") === rosIdParam(rosId))
|
||||
if (!identity) return reply.status(404).send({ error: "Клиент не найден" })
|
||||
const userName = parseIpsecUserComment(identity.comment) ?? ""
|
||||
const personal = (identity["mode-config"] ?? "").trim()
|
||||
|
||||
await deleteIdentity(client, identity[".id"]!)
|
||||
if (personal.startsWith("mc-ipsec-")) await deleteUserModeConfig(client, personal)
|
||||
if (removeCertificate && identity["remote-certificate"]) {
|
||||
await client.removeCertificate(identity["remote-certificate"]).catch(() => undefined)
|
||||
} else if (removeCertificate && userName) {
|
||||
await client.removeCertificate(clientCertName(userName)).catch(() => undefined)
|
||||
}
|
||||
await recordIpsec(server, "apply")
|
||||
return reply.send({ ok: true })
|
||||
} catch (e) {
|
||||
return errReply(reply, e)
|
||||
}
|
||||
})
|
||||
|
||||
app.post("/ipsec/users/:serverId/:rosId/cert", async (req, reply) => {
|
||||
const { serverId, rosId } = req.params as { serverId: string; rosId: string }
|
||||
const parsed = ipsecCertExportRequestSchema.safeParse({ ...(req.body as object), serverId, clientId: rosId })
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
||||
}
|
||||
const body = parsed.data
|
||||
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
|
||||
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
try {
|
||||
const state = await fetchIpsecState(server)
|
||||
const identity = state.identities.find((i) => String(i[".id"] ?? "") === rosIdParam(rosId))
|
||||
if (!identity) return reply.status(404).send({ error: "Клиент не найден" })
|
||||
const userName = parseIpsecUserComment(identity.comment) ?? ""
|
||||
if (!userName) return reply.status(400).send({ error: "Не managed-клиент" })
|
||||
const certName = identity["remote-certificate"]?.trim() || clientCertName(userName)
|
||||
if (!(await findCertificate(client, certName))) {
|
||||
return reply.status(404).send({ error: `Сертификат ${certName} не найден на роутере` })
|
||||
}
|
||||
const serverCert = state.certs.find((c) => String(c.name ?? "") === IPSEC_SERVER_CERT)
|
||||
const serverEndpoint = String(serverCert?.["common-name"] ?? "").trim() || server.host
|
||||
const sharedMc = state.modeConfigs.find((m) => (m.name ?? "").trim() === IPSEC_COMMON_NAME)
|
||||
const dns = sharedMc?.["static-dns"]?.trim() || undefined
|
||||
// export работает по имени сертификата (certName), не по userName
|
||||
const { fileName, content } = await client.exportCertificatePkcs12({
|
||||
name: certName,
|
||||
passphrase: body.passphrase,
|
||||
})
|
||||
const p12B64 = content.toString("base64")
|
||||
const bundle: IpsecCertBundle = {
|
||||
user: userName,
|
||||
serverEndpoint,
|
||||
filename: fileName,
|
||||
contentB64: p12B64,
|
||||
mime: "application/x-pkcs12",
|
||||
passphrase: body.passphrase,
|
||||
sswanFilename: `${certName}.sswan`,
|
||||
sswanContent: buildSswanConfig({
|
||||
name: `IKEv2 ${serverEndpoint}`,
|
||||
serverEndpoint,
|
||||
serverId: serverEndpoint,
|
||||
p12B64,
|
||||
}),
|
||||
instructions: buildClientInstructions({
|
||||
userName,
|
||||
serverEndpoint,
|
||||
p12Filename: fileName,
|
||||
passphrase: body.passphrase,
|
||||
dns,
|
||||
}),
|
||||
}
|
||||
return reply.send(bundle)
|
||||
} catch (e) {
|
||||
return errReply(reply, e)
|
||||
}
|
||||
})
|
||||
|
||||
app.get("/ipsec/revisions", async (req, reply) => {
|
||||
const q = req.query as { serverId?: string | number }
|
||||
const serverId = parseDbServerId(q.serverId)
|
||||
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
|
||||
const revisions = await listRevisions(serverId, "ipsec")
|
||||
return reply.send({ revisions })
|
||||
})
|
||||
|
||||
app.post("/ipsec/revisions/:id/restore", {
|
||||
schema: { params: RevisionIdParamSchema },
|
||||
}, async (req, reply) => {
|
||||
const { id } = req.params
|
||||
const body = req.body as { serverId?: string | number } | undefined
|
||||
const loaded = await loadRevisionForRestore({
|
||||
id,
|
||||
section: "ipsec",
|
||||
requestedServerId: parseDbServerId(body?.serverId),
|
||||
})
|
||||
if (!loaded.ok) return reply.status(loaded.status).send({ error: loaded.error })
|
||||
try {
|
||||
const desired = parseIpsecSnapshot(loaded.row.payload)
|
||||
const state = await fetchIpsecRestoreState(loaded.server)
|
||||
const ops = planIpsecRestore(desired, {
|
||||
peers: state.peers,
|
||||
identities: state.identities,
|
||||
modeConfigs: state.modeConfigs,
|
||||
pools: state.pools,
|
||||
nat: state.nat,
|
||||
})
|
||||
await executeRosOps(state.client, ops)
|
||||
await recordIpsec(loaded.server, "rollback")
|
||||
const result = await listIpsec({ serverId: String(loaded.server.id) })
|
||||
return reply.send({ ok: true, ...result })
|
||||
} catch (e) {
|
||||
return errReply(reply, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default ipsecRoutes
|
||||
@@ -2,6 +2,7 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
import { count } from "drizzle-orm"
|
||||
import { listCertificatesFromServers } from "../services/certificates-service.js"
|
||||
import { countWireGuardInterfaces } from "../services/wireguard-live.js"
|
||||
import { countIpsecClients } from "../services/ipsec-live.js"
|
||||
import { countVxlanTunnels } from "../services/vxlan-live.js"
|
||||
import { countContainers } from "../services/containers-live.js"
|
||||
import { countBgpSessions } from "../services/bgp-peers-live.js"
|
||||
@@ -27,9 +28,10 @@ const sidebarCountsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
const uptimeProbesTotal = await tableCount(uptimeProbes)
|
||||
const uptimeSpeedProbesTotal = await tableCount(uptimeSpeedProbes)
|
||||
const recursiveRoutesTotal = await tableCount(recursiveRoutes)
|
||||
const [certRes, wireguardTotal, bgpTotal, vxlanTotal, containersTotal] = await Promise.all([
|
||||
const [certRes, wireguardTotal, ipsecTotal, bgpTotal, vxlanTotal, containersTotal] = await Promise.all([
|
||||
listCertificatesFromServers(),
|
||||
countWireGuardInterfaces().catch(() => 0),
|
||||
countIpsecClients().catch(() => 0),
|
||||
countBgpSessions().catch(() => 0),
|
||||
countVxlanTunnels().catch(() => 0),
|
||||
countContainers().catch(() => 0),
|
||||
@@ -46,6 +48,7 @@ const sidebarCountsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
recursiveRoutes: recursiveRoutesTotal,
|
||||
certificates: certificatesTotal,
|
||||
wireguard: wireguardTotal,
|
||||
ipsec: ipsecTotal,
|
||||
users: usersTotal,
|
||||
bgpSessions: bgpTotal,
|
||||
vxlan: vxlanTotal,
|
||||
|
||||
Reference in new issue
Block a user