fix(ipsec): различать частичный и неподтверждённый результат операций

This commit is contained in:
Denozordec committed 2026-10-09 16:06:33 +07:00
1 parent a83e4734df
commit 43b00e0740
15 files changed
+263 -37

No files matched your search

+2 -2
View File
@@ -2,12 +2,12 @@ import assert from "node:assert/strict"
import Fastify from "fastify"
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
import { createIpsecRoutes } from "./ipsec.js"
import { MikrotikClient } from "../services/mikrotik.js"
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
import { fetchIpsecState } from "../services/ipsec-live.js"
const original = MikrotikClient.fromServer
const server = { id: 1, name: "test", host: "192.0.2.1", enabled: true } as Parameters<typeof fetchIpsecState>[0]
let patches = 0, recordings = 0, nativeDisabled = "false", rejectWrite = false
MikrotikClient.fromServer = () => ({ get: async (path: string) => path === "/ip/ipsec/peer" ? [{ ".id": "*P", name: "ike", "exchange-mode": "ike2", disabled: nativeDisabled }] : [], patch: async (_path: string, body: Record<string, string>) => { if (rejectWrite) throw Error("write rejected"); patches++; nativeDisabled = body.disabled! } }) as unknown as MikrotikClient
MikrotikClient.fromServer = () => ({ get: async (path: string) => path === "/ip/ipsec/peer" ? [{ ".id": "*P", name: "ike", "exchange-mode": "ike2", disabled: nativeDisabled }] : [], put: async () => {}, post: async () => {}, delete: async () => {}, patch: async (_path: string, body: Record<string, string>) => { if (rejectWrite) throw new MikrotikError(403, "/ip/ipsec/peer", "write rejected"); patches++; nativeDisabled = body.disabled! } }) as unknown as MikrotikClient
try {
for (const historySaved of [false, true]) {
patches = recordings = 0; nativeDisabled = "false"
+22
View File
@@ -0,0 +1,22 @@
import assert from "node:assert/strict"
import Fastify from "fastify"
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
import { createIpsecRoutes } from "./ipsec.js"
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
import { fetchIpsecState } from "../services/ipsec-live.js"
const server = { id: 1, name: "test", host: "192.0.2.1", enabled: true } as Parameters<typeof fetchIpsecState>[0]
const original = MikrotikClient.fromServer
let writes = 0, records = 0, fault = ""
const identities = [{ ".id": "*I", peer: "ike", "auth-method": "pre-shared-key", "remote-id": "key-id:alice", "mode-config": "mc-ipsec-alice", comment: "MikrotikManager:ipsec user=Alice" }, { ".id": "*OTHER", peer: "ike", "auth-method": "pre-shared-key", "remote-id": "key-id:other", "mode-config": "mc-ipsec-other", comment: "MikrotikManager:ipsec user=Other" }]
MikrotikClient.fromServer = () => ({ get: async (p: string) => p === "/ip/ipsec/peer" ? [{ ".id": "*P", name: "ike", "exchange-mode": "ike2" }] : p === "/ip/ipsec/identity" ? identities : p === "/ip/ipsec/mode-config" ? [{ ".id": "*MC", name: "mc-ipsec-alice", address: "198.18.81.250" }, { ".id": "*OTHERMC", name: "mc-ipsec-other", address: "198.18.81.249" }] : [], put: async () => { writes++ }, post: async () => { writes++ }, delete: async () => { writes++ }, patch: async (path: string, body: Record<string,string>) => { writes++; if (fault === "partial" && writes === 2) throw new MikrotikError(403, path, "rejected"); Object.assign(identities.find(i => path.endsWith(i[".id"]))!, body); if (fault === "lost-ack") throw Error("socket closed after application") } }) as unknown as MikrotikClient
const app = Fastify(); app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
await app.register(createIpsecRoutes({ findServer: async () => server, recordRevision: async () => { records++; return {created:false,revision:{} as never} } }), { prefix: "/api" })
const patch = (payload: Record<string, unknown>) => app.inject({ method: "PATCH", url: "/api/ipsec/users/1/identity:*I", payload })
try {
for (const body of [{name:"New",staticIp:"198.18.81.249"},{name:"Other"},{remoteId:"key-id:other"}]) { writes=0; const r=await patch(body); assert.equal(r.statusCode,409); assert.equal(writes,0); assert.equal(identities[0]!.comment,"MikrotikManager:ipsec user=Alice") }
writes=records=0; fault="partial"; let r=await patch({name:"New",psk:"new-test-secret"}); assert.equal(r.statusCode,502); assert.equal(r.json().state,"partial"); assert.equal(r.json().confirmedWrites,1); assert.equal(records,0); assert.equal(identities[0]!.comment,"MikrotikManager:ipsec user=New")
writes=records=0; fault="lost-ack"; r=await patch({name:"Unknown"}); assert.equal(r.statusCode,502); assert.equal(r.json().state,"unknown"); assert.equal(r.json().confirmedWrites,0); assert.equal(identities[0]!.comment,"MikrotikManager:ipsec user=Unknown"); assert.equal(records,0)
writes=records=0; fault=""; r=await patch({remoteId:"fresh"}); assert.equal(r.statusCode,200); assert.equal(identities[0]!["remote-id"],"key-id:fresh"); assert.equal(writes,1); assert.equal(records,1)
} finally { await app.close(); MikrotikClient.fromServer=original }
console.log("IPsec HTTP mutations: no-write conflicts, acknowledged partial failure, applied-but-lost ack and remote ID normalization")
+65 -26
View File
@@ -31,6 +31,7 @@ import {
selectIke2Peers,
selectSharedIke2Identity,
userModeConfigName,
normalizeIpsecRemoteId,
} from "../services/ipsec-config.js"
import {
ensureIpsecPeer,
@@ -78,6 +79,7 @@ import { removeManagedIpsecServer } from "../services/ipsec-cleanup.js"
import { parseDbServerId } from "../utils/server-id.js"
import { isIP } from "node:net"
import { randomBytes } from "node:crypto"
import { trackIpsecMutation, type IpsecMutationProgress } from "../services/ipsec-operation.js"
import { z } from "zod"
@@ -93,11 +95,14 @@ function rosIdParam(v: string): string {
return decodeURIComponent(v)
}
function errReply(reply: FastifyReply, e: unknown) {
function errReply(reply: FastifyReply, e: unknown, progress?: IpsecMutationProgress) {
if (e instanceof IpsecRestoreConflict) return reply.status(409).send({ error: `${e.message}. Изменений не было` })
if (e instanceof IpsecHistoryError) return reply.status(502).send({ error: e.message, state: "applied", historyRecorded: false })
const msg = e instanceof MikrotikError ? e.message : e instanceof Error ? e.message : String(e)
return reply.status(502).send({ error: `RouterOS: ${msg}` })
const receipt = progress?.receipt()
const guidance = receipt?.state === "applied" ? "Операция выполнена, но итоговый ответ не получен."
: receipt?.state === "partial" ? "Часть изменений применена." : receipt?.state === "unknown" ? "Результат операции не подтверждён." : ""
return reply.status(502).send({ error: `RouterOS: ${msg}${guidance ? `. ${guidance} Обновите данные перед повтором` : ""}`, ...receipt })
}
/**
@@ -206,7 +211,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
}
const server = await getEnabledIpsecServerById(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
const caCert = await ensureCaCertificate(client, body.caDaysValid)
const serverCert = await ensureServerCertificate(client, {
@@ -225,11 +231,12 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
if (body.createNatRule) {
await ensureNatRule(client, body.poolCidr, ipsecManagedComment("интернет клиентам VPN"))
}
progress.complete()
await recordIpsec(server, "apply")
const state = await fetchIpsecState(server)
return reply.status(201).send(mapServerSummary(state, mapClients(state)))
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -239,13 +246,15 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const removeCertificates = q.removeCertificates !== "false"
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
await removeManagedIpsecServer(client, removeCertificates)
progress.complete()
await recordIpsec(server, "apply")
return reply.send({ ok: true })
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -257,7 +266,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const body = parsed.data
const server = await getEnabledIpsecServerById(body.serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
const state = await fetchIpsecState(server)
const ike2Peers = selectIke2Peers(state.peers)
@@ -289,6 +299,7 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
if (body.peerName && (peer.name ?? "").trim() !== body.peerName.trim()) return reply.status(404).send({ error: "Выбранный peer не найден" })
if (mapClients(state).some(c => c.name === body.name.trim()) || (body.authMethod === "certificate" && state.certs.some(c => c.name === clientCertName(body.name)))) return reply.status(409).send({ error: "Клиент уже существует" })
const peerName = (peer.name ?? "").trim()
if (body.authMethod === "pre-shared-key" && state.identities.some(i => i.peer === peerName && i["auth-method"] === "pre-shared-key" && i["remote-id"] === normalizeIpsecRemoteId(body.remoteId ?? body.name))) return reply.status(409).send({ error: "Remote ID уже используется клиентом этого peer" })
const serverEndpoint = String(serverCertRow?.["common-name"] ?? "").trim() || server.host
const dns = sharedMc?.["static-dns"]?.trim() || undefined
@@ -319,7 +330,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
daysValid: body.daysValid ?? 1825,
})
if (issued.existed) {
return reply.status(409).send({ error: `Клиент с сертификатом ${issued.certName} уже существует` })
const receipt = progress.receipt()
return reply.status(409).send({ error: `Клиент с сертификатом ${issued.certName} уже существует${receipt ? ". Часть изменений применена; обновите данные перед повтором" : ""}`, ...receipt })
}
certName = issued.certName
}
@@ -360,6 +372,7 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
})
}
progress.complete()
await recordIpsec(server, "apply")
const fresh = await fetchIpsecState(server)
const clients = mapClients(fresh)
@@ -368,7 +381,7 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
) ?? clients.find((c) => c.certificateName === certName)
return reply.status(201).send({ client: created ?? null, bundle })
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -382,7 +395,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
if ("staticIp" in d && d.staticIp && isIP(d.staticIp.trim()) !== 4) return reply.status(400).send({ error: "Некорректный статический IPv4 адрес" })
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
const state = await fetchIpsecState(server)
const targetRosId = rosIdParam(rosId)
@@ -407,6 +421,16 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const sharedMc = state.modeConfigs.find((m) => (m.name ?? "").trim() === IPSEC_COMMON_NAME)
const sharedMcName = (sharedMc?.name ?? "").trim()
const newName = d.name?.trim() || oldName
const normalizedRemoteId = d.remoteId === undefined ? undefined : normalizeIpsecRemoteId(d.remoteId)
if (d.name && newName !== oldName && mapClients(state).some(c => c.name === newName && (identity ? c.rosId !== `identity:${identity[".id"]}` : c.certName !== certRow?.name))) return reply.status(409).send({ error: "Имя уже используется другим клиентом" })
if (identity && normalizedRemoteId !== undefined && state.identities.some(i => i[".id"] !== identity[".id"] && i.peer === identity.peer && i["auth-method"] === "pre-shared-key" && i["remote-id"] === normalizedRemoteId)) return reply.status(409).send({ error: "Remote ID уже используется клиентом этого peer" })
// Validate conflicts before changing even the display name.
if (d.staticIp) {
const currentMc = (identity?.["mode-config"] ?? "").trim()
if (takenStaticIps(state.modeConfigs.filter(m => !currentMc || m.name !== currentMc)).includes(d.staticIp.trim())) return reply.status(409).send({ error: "IP уже назначен другому клиенту" })
}
if (d.name && d.name !== oldName) {
if (identity) {
@@ -440,6 +464,7 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const name = userModeConfigName(newName)
await putUserModeConfig(client, name, ip, ipsecManagedComment(`клиент ${newName.trim()}`))
await patchIdentity(client, identity[".id"]!, { "mode-config": name })
if (currentMc && currentMc !== sharedMcName && currentMc !== name) await deleteUserModeConfig(client, currentMc)
}
} else if (!identity && certRow && d.staticIp) {
// cert-клиент без identity: персональная identity (match-by=certificate) + статический mode-config
@@ -466,16 +491,17 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
if (identity) {
const patch: Record<string, string> = {}
if (d.psk) patch.secret = d.psk
if (d.remoteId !== undefined) patch["remote-id"] = d.remoteId
if (d.remoteId !== undefined) patch["remote-id"] = normalizedRemoteId!
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)
}
progress.complete()
await recordIpsec(server, "apply")
return reply.send({ ok: true })
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -485,7 +511,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const removeCertificate = q.removeCertificate !== "false"
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
const state = await fetchIpsecState(server)
const targetRosId = rosIdParam(rosId)
@@ -518,10 +545,11 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
await client.removeCertificate(certName)
}
}
progress.complete()
await recordIpsec(server, "apply")
return reply.send({ ok: true })
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -534,7 +562,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const d = parsed.data
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
const state = await fetchIpsecState(server)
const peer = state.peers.find((p) => String(p[".id"] ?? "") === rosIdParam(rosId))
@@ -548,10 +577,11 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
if (d.profile !== undefined) body.profile = d.profile
if (d.disabled !== undefined) body.disabled = d.disabled ? "yes" : "no"
if (Object.keys(body).length) await patchPeer(client, rosIdParam(rosId), body)
progress.complete()
await recordIpsec(server, "apply")
return reply.send({ ok: true })
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -560,7 +590,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const q = req.query as { force?: string }
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
const state = await fetchIpsecState(server)
const peer = state.peers.find((p) => String(p[".id"] ?? "") === rosIdParam(rosId))
@@ -576,10 +607,11 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
})
}
await deletePeer(client, rosIdParam(rosId))
progress.complete()
await recordIpsec(server, "apply")
return reply.send({ ok: true })
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -592,7 +624,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const q = req.query as { force?: string }
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
const state = await fetchIpsecState(server)
const name = parsed.data.name
@@ -611,10 +644,11 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
})
}
await client.delete(`/certificate/${encodeURIComponent(cert[".id"])}`)
progress.complete()
await recordIpsec(server, "apply")
return reply.send({ ok: true })
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -627,7 +661,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const body = parsed.data
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
const state = await fetchIpsecState(server)
const target = resolveIpsecClientTarget(state, rosIdParam(rosId))
@@ -674,7 +709,7 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
}
return reply.send(bundle)
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -688,7 +723,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
const { name, passphrase } = parsed.data
const server = await getEnabledIpsecServerById(serverIdParam(serverId))
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const client = MikrotikClient.fromServer(server)
const progress = trackIpsecMutation(MikrotikClient.fromServer(server))
const client = progress.client
try {
const state = await fetchIpsecState(server)
const cert = state.certs.find((c) => String(c.name ?? "").trim() === name)
@@ -707,7 +743,7 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
})
return reply.send(bundle)
} catch (e) {
return errReply(reply, e)
return errReply(reply, e, progress)
}
})
@@ -730,6 +766,7 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
requestedServerId: parseDbServerId(body?.serverId),
})
if (!loaded.ok) return reply.status(loaded.status).send({ error: loaded.error })
let progress: IpsecMutationProgress | undefined
try {
const desired = parseIpsecSnapshot(loaded.row.payload)
const state = await fetchIpsecRestoreState(loaded.server)
@@ -746,12 +783,14 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs
nat: state.nat,
policies: state.policies,
})
await executeRosOps(state.client, ops)
progress = trackIpsecMutation(state.client)
await executeRosOps(progress.client, ops)
progress.complete()
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)
return errReply(reply, e, progress)
}
})
}
+5
View File
@@ -311,3 +311,8 @@ export function buildClientInstructions(args: {
"Адрес выдаётся автоматически при подключении (mode-config).",
].join("\n")
}
export function normalizeIpsecRemoteId(value: string): string {
const id = value.trim()
return id && !id.includes(":") && !["auto", "ignore"].includes(id) ? `key-id:${id}` : id
}
@@ -0,0 +1,20 @@
import assert from "node:assert/strict"
import { trackIpsecMutation } from "./ipsec-operation.js"
import { MikrotikError, type MikrotikClient } from "./mikrotik.js"
const rejected = new MikrotikError(403, "/ip/ipsec/peer", "forbidden")
for (const steps of [0, 1]) for (const failure of [rejected, new Error("socket closed"), new MikrotikError(500, "/ip/ipsec/peer", "server error")]) {
let attempts = 0
const client = { patch: async () => { attempts++; if (attempts > steps) throw failure }, put: async () => {}, delete: async () => {}, post: async () => {} } as unknown as MikrotikClient
const tracked = trackIpsecMutation(client)
if (steps) await tracked.client.patch("/own", {})
await assert.rejects(tracked.client.patch("/own", {}))
assert.equal(tracked.receipt()?.state, failure === rejected ? steps ? "partial" : undefined : "unknown")
assert.equal(tracked.receipt()?.confirmedWrites ?? 0, steps)
}
const client = { patch: async () => {}, put: async () => {}, delete: async () => {}, post: async () => {}, removeCertificate: async function () { await this.delete("/certificate/own") } } as unknown as MikrotikClient
const tracked = trackIpsecMutation(client)
await tracked.client.removeCertificate("own")
assert.equal(tracked.receipt()?.confirmedWrites, 1)
tracked.complete(); assert.deepEqual(tracked.receipt(), { state: "applied", confirmedWrites: 1, needsReview: true })
console.log("IPsec progress: native 4xx, partial writes, transport/5xx uncertainty, nested methods and completed operation")
+27
View File
@@ -0,0 +1,27 @@
import { MikrotikClient, MikrotikError } from "./mikrotik.js"
/** Request-local evidence only: never claims that an unacknowledged request failed to apply. */
export function trackIpsecMutation(client: MikrotikClient) {
let confirmedWrites = 0, uncertainWrite = false, completed = false
const methods = client as unknown as Record<string, (...args: unknown[]) => Promise<unknown>>
for (const method of ["put", "patch", "delete", "post"]) {
const original = methods[method]!.bind(client)
methods[method] = async (...args) => {
try { const result = await original(...args); confirmedWrites++; return result }
catch (error) {
if (!(error instanceof MikrotikError && error.statusCode >= 400 && error.statusCode < 500)) uncertainWrite = true
throw error
}
}
}
return {
client,
complete: () => { completed = true },
receipt: () => {
const state = completed ? "applied" : uncertainWrite ? "unknown" : confirmedWrites ? "partial" : undefined
return state ? { state, confirmedWrites, needsReview: true } : undefined
},
}
}
export type IpsecMutationProgress = ReturnType<typeof trackIpsecMutation>
+2 -2
View File
@@ -1,5 +1,5 @@
import type { MikrotikClient } from "./mikrotik.js"
import {
import { normalizeIpsecRemoteId,
ipsecManagedComment,
isManagedIpsecModeConfig,
selectIke2Peers,
@@ -197,7 +197,7 @@ export function identityRosBody(f: IdentityFields): Record<string, string> {
"remote-certificate": f.remoteCertificate,
"match-by": f.authMethod === "certificate" && f.remoteCertificate ? "certificate" : undefined,
secret: f.secret,
"remote-id": f.remoteId && !f.remoteId.includes(":") && !["auto", "ignore"].includes(f.remoteId) ? `key-id:${f.remoteId}` : f.remoteId,
"remote-id": f.remoteId === undefined ? undefined : normalizeIpsecRemoteId(f.remoteId),
"mode-config": f.modeConfig,
"generate-policy": "port-strict",
comment: f.comment,