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

+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,