fix(ipsec): обеспечить NAT и изоляцию политик для передачи данных
This commit is contained in:
1 parent
43b00e0740
commit
89de7e134b
17 files changed
+571
-25
No files matched your search
@@ -170,7 +170,7 @@ import { fingerprintPayload, revisionItemCount } from "./config-revisions.js"
|
||||
identities: [{ peerName: "ipsec-vpn", authMethod: "rsa-key", certificate: "ipsec-server", remoteCertificate: "ipsec-user-alice", matchBy: "certificate", secret: "", remoteId: "", modeConfig: "ipsec-vpn", generatePolicy: "port-strict", comment: "MikrotikManager:ipsec user=alice", disabled: false }],
|
||||
modeConfigs: [{ name: "ipsec-vpn", addressPool: "ipsec-vpn", address: "", staticDns: "10.77.0.1", comment: "MikrotikManager:ipsec" }],
|
||||
pools: [{ name: "ipsec-vpn", ranges: "10.77.0.2-10.77.0.254", comment: "MikrotikManager:ipsec" }],
|
||||
nat: [{ chain: "srcnat", action: "masquerade", srcAddress: "10.77.0.0/24", comment: "MikrotikManager:ipsec интернет клиентам VPN" }],
|
||||
nat: [{ chain: "srcnat", action: "masquerade", srcAddress: "10.77.0.0/24", outInterface: "ether1", ipsecPolicy: "out,none", comment: "MikrotikManager:ipsec интернет клиентам VPN" }],
|
||||
})
|
||||
// round-trip через payload ревизии
|
||||
const restored = parseIpsecSnapshot(JSON.parse(JSON.stringify(desired)))
|
||||
|
||||
@@ -190,6 +190,7 @@ export interface IpsecSnapshotIdentity {
|
||||
generatePolicy: string
|
||||
comment: string
|
||||
disabled: boolean
|
||||
policyTemplateGroup?: string
|
||||
}
|
||||
|
||||
export interface IpsecSnapshotModeConfig {
|
||||
@@ -218,6 +219,9 @@ export interface IpsecSnapshotNat {
|
||||
action: string
|
||||
srcAddress: string
|
||||
comment: string
|
||||
outInterface?: string
|
||||
outInterfaceList?: string
|
||||
ipsecPolicy?: string
|
||||
}
|
||||
|
||||
export interface IpsecSnapshot {
|
||||
@@ -626,6 +630,7 @@ function canonicalIpsecIdentityRaw(i: Partial<IpsecSnapshotIdentity>): IpsecSnap
|
||||
remoteId: str(i.remoteId),
|
||||
modeConfig: str(i.modeConfig),
|
||||
generatePolicy: str(i.generatePolicy),
|
||||
...(str(i.policyTemplateGroup) ? { policyTemplateGroup: str(i.policyTemplateGroup) } : {}),
|
||||
comment: str(i.comment),
|
||||
disabled: Boolean(i.disabled),
|
||||
}
|
||||
@@ -668,7 +673,7 @@ export function canonicalIpsecSnapshot(input: {
|
||||
.filter((p) => p.dstAddress)
|
||||
.sort((a, b) => a.dstAddress.localeCompare(b.dstAddress)),
|
||||
nat: (input.nat ?? [])
|
||||
.map((n) => ({ chain: str(n.chain), action: str(n.action), srcAddress: str(n.srcAddress), comment: str(n.comment) }))
|
||||
.map((n) => ({ chain: str(n.chain), action: str(n.action), srcAddress: str(n.srcAddress), comment: str(n.comment), ...(str(n.outInterface) ? { outInterface: str(n.outInterface) } : {}), ...(str(n.outInterfaceList) ? { outInterfaceList: str(n.outInterfaceList) } : {}), ...(str(n.ipsecPolicy) ? { ipsecPolicy: str(n.ipsecPolicy) } : {}) }))
|
||||
.filter((n) => n.comment)
|
||||
.sort((a, b) => a.comment.localeCompare(b.comment)),
|
||||
}
|
||||
@@ -712,6 +717,7 @@ function ipsecIdentityBody(i: IpsecSnapshotIdentity): Record<string, string> {
|
||||
"remote-id": i.remoteId,
|
||||
"mode-config": i.modeConfig,
|
||||
"generate-policy": i.generatePolicy,
|
||||
"policy-template-group": i.policyTemplateGroup,
|
||||
comment: i.comment,
|
||||
disabled: rosYesNo(i.disabled),
|
||||
})
|
||||
@@ -733,12 +739,16 @@ function ipsecPoolBody(p: IpsecSnapshotPool): Record<string, string> {
|
||||
}
|
||||
|
||||
function ipsecNatBody(n: IpsecSnapshotNat): Record<string, string> {
|
||||
return compactBody({
|
||||
const body = compactBody({
|
||||
chain: n.chain,
|
||||
action: n.action,
|
||||
"src-address": n.srcAddress,
|
||||
comment: n.comment,
|
||||
"ipsec-policy": n.ipsecPolicy,
|
||||
"out-interface": n.outInterface,
|
||||
"out-interface-list": n.outInterfaceList,
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
/** Restore только managed-объектов IKEv2 (peer/identity/mode-config/pool/nat; секреты (hidden) не перезаписываем;
|
||||
@@ -757,11 +767,16 @@ export function planIpsecRestore(
|
||||
const desired = canonicalIpsecSnapshot(desiredInput)
|
||||
const ops: RosWriteOp[] = []
|
||||
const removeDependents: RosWriteOp[] = []
|
||||
if (desired.nat.some(n => n.action === "masquerade" && !n.outInterface && !n.outInterfaceList)) throw new IpsecRestoreConflict("Ревизия NAT не содержит исходящий интерфейс. Настройте NAT вручную или используйте новую ревизию")
|
||||
const managedPeers = current.peers.filter((p) => isIpsecManagedComment(p.comment))
|
||||
const managedIdentities = current.identities.filter((i) => isIpsecManagedComment(i.comment))
|
||||
const managedModeConfigs = current.modeConfigs.filter(isManagedIpsecModeConfig)
|
||||
const managedPools = current.pools.filter((p) => isIpsecManagedComment(p.comment))
|
||||
const managedNat = current.nat.filter((n) => isIpsecManagedComment(n.comment))
|
||||
for (const want of desired.nat) {
|
||||
const live = managedNat.find(n => n.comment === want.comment)
|
||||
if (live && ((live.outInterface && !want.outInterface) || (live.outInterfaceList && !want.outInterfaceList))) throw new IpsecRestoreConflict("Откат требует снять прежнее ограничение интерфейса NAT. Снимите его вручную перед повтором")
|
||||
}
|
||||
|
||||
const removedPeers = managedPeers.filter(p => !desired.peers.some(w => w.name === p.name))
|
||||
const removedConfigs = managedModeConfigs.filter(m => !desired.modeConfigs.some(w => w.name === m.name))
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import type { MikrotikClient } from "./mikrotik.js"
|
||||
import { IPSEC_CA_CERT, IPSEC_SERVER_CERT, isManagedIpsecModeConfig, isIpsecManagedComment } from "./ipsec-config.js"
|
||||
import { IPSEC_CA_CERT, IPSEC_SERVER_CERT, IPSEC_POLICY_GROUP, isManagedIpsecModeConfig, isIpsecManagedComment } from "./ipsec-config.js"
|
||||
import { listByPath } from "./ipsec-ros.js"
|
||||
|
||||
/** Read the whole deletion scope before writing; never treat unreadable state as empty. */
|
||||
export async function removeManagedIpsecServer(client: MikrotikClient, removeCertificates: boolean): Promise<void> {
|
||||
const paths = ["/ip/ipsec/identity", "/ip/ipsec/peer", "/ip/ipsec/policy", "/ip/ipsec/mode-config", "/ip/pool", "/ip/firewall/nat"]
|
||||
const paths = ["/ip/ipsec/identity", "/ip/ipsec/peer", "/ip/ipsec/policy", "/ip/ipsec/mode-config", "/ip/pool", "/ip/firewall/nat", "/ip/ipsec/policy/group"]
|
||||
if (removeCertificates) paths.push("/certificate")
|
||||
const snapshots = await Promise.all(paths.map(async path => ({ path, rows: await listByPath(client, path) })))
|
||||
const owned = (path: string, row: Record<string, string | undefined>) => isIpsecManagedComment(row.comment) || (path === "/ip/ipsec/mode-config" && isManagedIpsecModeConfig(row)) || (path === "/certificate" && (row.name === IPSEC_CA_CERT || row.name === IPSEC_SERVER_CERT || row.name?.startsWith("ipsec-user-")))
|
||||
const linkedGroups = new Set(snapshots.find(s => s.path === "/ip/ipsec/policy")?.rows.filter(r => isIpsecManagedComment(r.comment)).map(r => r.group))
|
||||
const owned = (path: string, row: Record<string, string | undefined>) => path === "/ip/ipsec/policy/group" ? row.name === IPSEC_POLICY_GROUP && linkedGroups.has(row.name) : isIpsecManagedComment(row.comment) || (path === "/ip/ipsec/mode-config" && isManagedIpsecModeConfig(row)) || (path === "/certificate" && (row.name === IPSEC_CA_CERT || row.name === IPSEC_SERVER_CERT || row.name?.startsWith("ipsec-user-")))
|
||||
const names = (path: string) => new Set(snapshots.find(s => s.path === path)?.rows.filter(row => owned(path, row)).map(row => row.name).filter((name): name is string => typeof name === "string" && name.length > 0))
|
||||
const peerNames = names("/ip/ipsec/peer"), modeConfigs = names("/ip/ipsec/mode-config"), certificates = names("/certificate")
|
||||
const peerNames = names("/ip/ipsec/peer"), modeConfigs = names("/ip/ipsec/mode-config"), certificates = names("/certificate"), groups = names("/ip/ipsec/policy/group")
|
||||
for (const { path, rows } of snapshots) {
|
||||
for (const row of rows.filter(row => !owned(path, row))) {
|
||||
if (path === "/certificate" && certificates.has(row.ca ?? "")) throw new Error("Удаление заблокировано: управляемый CA подписал чужой сертификат")
|
||||
if (path === "/ip/ipsec/identity" && (peerNames.has(row.peer ?? "") || modeConfigs.has(row["mode-config"] ?? "") || certificates.has(row.certificate ?? "") || certificates.has(row["remote-certificate"] ?? ""))) throw new Error("Удаление заблокировано: объекты IPsec используются чужой identity")
|
||||
if (path === "/ip/ipsec/identity" && (groups.has(row["policy-template-group"] ?? "") || peerNames.has(row.peer ?? "") || modeConfigs.has(row["mode-config"] ?? "") || certificates.has(row.certificate ?? "") || certificates.has(row["remote-certificate"] ?? ""))) throw new Error("Удаление заблокировано: объекты IPsec используются чужой identity")
|
||||
if (path === "/ip/ipsec/policy" && row.dynamic !== "true" && row.dynamic !== "yes" && (row.peer ?? "").split(",").some(peer => peerNames.has(peer.trim()))) throw new Error("Удаление заблокировано: peer используется чужой policy")
|
||||
if (path === "/ip/ipsec/policy" && row.dynamic !== "true" && row.dynamic !== "yes" && groups.has(row.group ?? "")) throw new Error("Удаление заблокировано: группа используется чужой policy")
|
||||
}
|
||||
}
|
||||
for (const { path, rows } of snapshots) {
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface Ike2IdentityLike extends IdentityLike {
|
||||
certificate?: string
|
||||
"auth-method"?: string
|
||||
"mode-config"?: string
|
||||
"policy-template-group"?: string
|
||||
}
|
||||
|
||||
/** Peer (/ip/ipsec/peer). */
|
||||
@@ -95,6 +96,7 @@ export const IPSEC_CA_CERT = "ipsec-ca"
|
||||
export const IPSEC_SERVER_CERT = "ipsec-server"
|
||||
/** Общее имя peer / profile / proposal / пула / общего mode-config. */
|
||||
export const IPSEC_COMMON_NAME = "ipsec-vpn"
|
||||
export const IPSEC_POLICY_GROUP = "mmapp-ipsec-vpn"
|
||||
|
||||
export function ipsecSlug(name: string): string {
|
||||
return name
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface RosIpsecIdentity {
|
||||
"remote-id"?: string
|
||||
"mode-config"?: string
|
||||
"generate-policy"?: string
|
||||
"policy-template-group"?: string
|
||||
disabled?: string
|
||||
comment?: string
|
||||
}
|
||||
@@ -102,6 +103,9 @@ export interface RosFirewallNat {
|
||||
action?: string
|
||||
"src-address"?: string
|
||||
comment?: string
|
||||
"out-interface"?: string
|
||||
"out-interface-list"?: string
|
||||
"ipsec-policy"?: string
|
||||
}
|
||||
|
||||
export interface RosIpsecActivePeer {
|
||||
@@ -456,6 +460,7 @@ export async function captureIpsecSnapshot(server: ServerRow): Promise<IpsecSnap
|
||||
remoteId: (i["remote-id"] ?? "").trim(),
|
||||
modeConfig: (i["mode-config"] ?? "").trim(),
|
||||
generatePolicy: (i["generate-policy"] ?? "").trim(),
|
||||
policyTemplateGroup: i["policy-template-group"],
|
||||
comment: (i.comment ?? "").trim(),
|
||||
disabled: asBool(i.disabled),
|
||||
})),
|
||||
@@ -489,6 +494,9 @@ export async function captureIpsecSnapshot(server: ServerRow): Promise<IpsecSnap
|
||||
chain: (n.chain ?? "").trim(),
|
||||
action: (n.action ?? "").trim(),
|
||||
srcAddress: (n["src-address"] ?? "").trim(),
|
||||
outInterface: n["out-interface"],
|
||||
outInterfaceList: n["out-interface-list"],
|
||||
ipsecPolicy: n["ipsec-policy"],
|
||||
comment: (n.comment ?? "").trim(),
|
||||
})),
|
||||
})
|
||||
@@ -534,6 +542,7 @@ export async function fetchIpsecRestoreState(server: ServerRow): Promise<{
|
||||
remoteId: (i["remote-id"] ?? "").trim(),
|
||||
modeConfig: (i["mode-config"] ?? "").trim(),
|
||||
generatePolicy: (i["generate-policy"] ?? "").trim(),
|
||||
policyTemplateGroup: i["policy-template-group"],
|
||||
comment: (i.comment ?? "").trim(),
|
||||
disabled: asBool(i.disabled),
|
||||
})),
|
||||
@@ -559,6 +568,9 @@ export async function fetchIpsecRestoreState(server: ServerRow): Promise<{
|
||||
chain: (n.chain ?? "").trim(),
|
||||
action: (n.action ?? "").trim(),
|
||||
srcAddress: (n["src-address"] ?? "").trim(),
|
||||
outInterface: n["out-interface"],
|
||||
outInterfaceList: n["out-interface-list"],
|
||||
ipsecPolicy: n["ipsec-policy"],
|
||||
comment: (n.comment ?? "").trim(),
|
||||
})),
|
||||
snapshot: await captureIpsecSnapshot(server),
|
||||
|
||||
@@ -27,3 +27,11 @@ assert.throws(() => planIpsecRestore(canonicalIpsecSnapshot({}), { ...base, poli
|
||||
const foreignPool = { ...canonicalIpsecSnapshot({ pools: [{ name: peer.name, comment: "foreign" }] }).pools[0]!, rosId: "*FOREIGNPOOL" }
|
||||
assert.throws(() => planIpsecRestore(canonicalIpsecSnapshot({ pools: [{ name: peer.name, comment: "MikrotikManager:ipsec" }] }), { ...base, pools: [foreignPool] }), /чужой конфигурацией/)
|
||||
console.log("IPsec restore safety: foreign identity/policy references and name conflict fail before operations")
|
||||
const scopedNat = canonicalIpsecSnapshot({nat:[{chain:"srcnat",action:"masquerade",srcAddress:"198.18.82.0/24",outInterface:"ether1",ipsecPolicy:"out,none",comment:"MikrotikManager:ipsec NAT"}]})
|
||||
const scopedOps = planIpsecRestore(scopedNat,{peers:[],identities:[],modeConfigs:[],pools:[],nat:[]})
|
||||
const natOp=scopedOps.find(o=>o.op==="put"&&o.path==="/ip/firewall/nat")
|
||||
assert(natOp&&"body" in natOp&&natOp.body["out-interface"]==="ether1"&&natOp.body["ipsec-policy"]==="out,none")
|
||||
assert.throws(()=>planIpsecRestore(canonicalIpsecSnapshot({nat:[{chain:"srcnat",action:"masquerade",srcAddress:"198.18.82.0/24",comment:"MikrotikManager:ipsec NAT"}]}),{peers:[],identities:[],modeConfigs:[],pools:[],nat:[]}),/Ревизия NAT/)
|
||||
console.log("IPsec NAT history: retains egress/policy, rejects legacy unscoped masquerade")
|
||||
assert.throws(()=>planIpsecRestore(scopedNat,{peers:[],identities:[],modeConfigs:[],pools:[],nat:[{...scopedNat.nat[0]!,outInterface:undefined,outInterfaceList:"WAN",rosId:"*N"}]}),/Снимите его вручную/)
|
||||
assert(natOp&&"body" in natOp&&natOp.body["out-interface-list"]===undefined,"Never send empty ambiguous RouterOS list value")
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { MikrotikClient } from "./mikrotik.js"
|
||||
import { normalizeIpsecRemoteId,
|
||||
IPSEC_POLICY_GROUP,
|
||||
isIpsecManagedComment,
|
||||
ipsecManagedComment,
|
||||
isManagedIpsecModeConfig,
|
||||
selectIke2Peers,
|
||||
@@ -104,6 +106,7 @@ export async function ensurePolicyTemplate(
|
||||
"src-address": "0.0.0.0/0",
|
||||
"dst-address": dstCidr,
|
||||
proposal: proposalName,
|
||||
group: IPSEC_POLICY_GROUP,
|
||||
comment,
|
||||
}))
|
||||
return
|
||||
@@ -112,11 +115,24 @@ export async function ensurePolicyTemplate(
|
||||
"src-address": "0.0.0.0/0",
|
||||
"dst-address": dstCidr,
|
||||
proposal: proposalName,
|
||||
group: IPSEC_POLICY_GROUP,
|
||||
template: "yes",
|
||||
comment,
|
||||
}))
|
||||
}
|
||||
|
||||
export class IpsecPolicyGroupConflict extends Error {}
|
||||
|
||||
/** Reserved group reuse requires a managed template and no foreign consumers. */
|
||||
export async function ipsecPolicyGroupExists(client: MikrotikClient): Promise<boolean> {
|
||||
const groups = await listByPath(client, "/ip/ipsec/policy/group")
|
||||
if (!groups.some(g => g.name === IPSEC_POLICY_GROUP)) return false
|
||||
const [policies, identities] = await Promise.all([listByPath(client, "/ip/ipsec/policy"), listByPath(client, "/ip/ipsec/identity")])
|
||||
if (!policies.some(p => p.group === IPSEC_POLICY_GROUP && isIpsecManagedComment(p.comment))) throw new IpsecPolicyGroupConflict("Зарезервированное имя группы политик занято объектом без подтверждённого управления")
|
||||
if (policies.some(p => p.group === IPSEC_POLICY_GROUP && !isIpsecManagedComment(p.comment) && p.dynamic !== "true" && p.dynamic !== "yes") || identities.some(i => i["policy-template-group"] === IPSEC_POLICY_GROUP && !isIpsecManagedComment(i.comment))) throw new IpsecPolicyGroupConflict("Группа политик используется чужой конфигурацией")
|
||||
return true
|
||||
}
|
||||
|
||||
export async function ensureIpsecPeer(
|
||||
client: MikrotikClient,
|
||||
name: string,
|
||||
@@ -134,20 +150,44 @@ export async function ensureIpsecPeer(
|
||||
}))
|
||||
}
|
||||
|
||||
/** Managed srcnat masquerade: интернет клиентам VPN. */
|
||||
export async function ensureNatRule(client: MikrotikClient, poolCidr: string, comment: string): Promise<void> {
|
||||
export class IpsecNatInterfaceConflict extends Error {}
|
||||
|
||||
export function selectIpsecNatInterface(routes: RosRow[], interfaces: RosRow[]): string {
|
||||
const candidates = new Set<string>()
|
||||
for (const route of routes) {
|
||||
if (route["dst-address"] !== "0.0.0.0/0" || !["true", "yes"].includes(route.active ?? "") || (route["routing-table"] && route["routing-table"] !== "main")) continue
|
||||
const hops = (route["immediate-gw"] ?? route.gateway ?? "").split(",")
|
||||
for (const hop of hops) {
|
||||
const name = hop.includes("%") ? hop.slice(hop.lastIndexOf("%") + 1).trim() : hop.trim()
|
||||
if (interfaces.some(i => i.name === name && !["true", "yes"].includes(i.disabled ?? ""))) candidates.add(name)
|
||||
}
|
||||
}
|
||||
if (candidates.size !== 1) throw new IpsecNatInterfaceConflict("Не удалось однозначно определить исходящий интерфейс по активному маршруту main. Отключите NAT в мастере и настройте правило вручную")
|
||||
return [...candidates][0]!
|
||||
}
|
||||
|
||||
export async function resolveIpsecNatInterface(client: MikrotikClient): Promise<string> {
|
||||
const [routes, interfaces] = await Promise.all([listByPath(client, "/ip/route"), listByPath(client, "/interface")])
|
||||
return selectIpsecNatInterface(routes, interfaces)
|
||||
}
|
||||
|
||||
/** Managed srcnat masquerade only through the verified default-route egress. */
|
||||
export async function ensureNatRule(client: MikrotikClient, poolCidr: string, comment: string, outInterface: string): Promise<void> {
|
||||
const rows = await listByPath(client, "/ip/firewall/nat")
|
||||
const existing = rows.find(
|
||||
(r) => (r.comment ?? "").trim() === comment && r.chain === "srcnat",
|
||||
)
|
||||
if (existing?.[".id"]) return
|
||||
await client.put("/ip/firewall/nat", toRosBody({
|
||||
const body = toRosBody({
|
||||
chain: "srcnat",
|
||||
action: "masquerade",
|
||||
"src-address": poolCidr,
|
||||
"out-interface-list": "WAN",
|
||||
"out-interface": outInterface,
|
||||
"ipsec-policy": "out,none",
|
||||
comment,
|
||||
}))
|
||||
})
|
||||
if (existing?.[".id"]) {
|
||||
await client.patch(`/ip/firewall/nat/${encodeURIComponent(existing[".id"])}`, body)
|
||||
} else await client.put("/ip/firewall/nat", body)
|
||||
}
|
||||
|
||||
// ── клиенты ─────────────────────────────────────────────────────────────────
|
||||
@@ -187,6 +227,7 @@ export interface IdentityFields {
|
||||
remoteId?: string
|
||||
/** Legacy hint: RouterOS identities do not accept place-before; specificity uses match-by/remote-certificate. */
|
||||
placeBefore?: string
|
||||
policyTemplateGroup?: string
|
||||
}
|
||||
|
||||
export function identityRosBody(f: IdentityFields): Record<string, string> {
|
||||
@@ -200,11 +241,16 @@ export function identityRosBody(f: IdentityFields): Record<string, string> {
|
||||
"remote-id": f.remoteId === undefined ? undefined : normalizeIpsecRemoteId(f.remoteId),
|
||||
"mode-config": f.modeConfig,
|
||||
"generate-policy": "port-strict",
|
||||
"policy-template-group": f.policyTemplateGroup,
|
||||
comment: f.comment,
|
||||
})
|
||||
}
|
||||
|
||||
export async function putIdentity(client: MikrotikClient, fields: IdentityFields): Promise<void> {
|
||||
if (!fields.policyTemplateGroup) {
|
||||
const shared = selectSharedIke2Identity(await listByPath(client, "/ip/ipsec/identity"), [fields.peerName])
|
||||
fields = { ...fields, policyTemplateGroup: shared?.["policy-template-group"] }
|
||||
}
|
||||
await client.put("/ip/ipsec/identity", identityRosBody(fields))
|
||||
}
|
||||
|
||||
@@ -214,7 +260,7 @@ export async function putIdentity(client: MikrotikClient, fields: IdentityFields
|
||||
*/
|
||||
export async function ensureSharedIke2Identity(
|
||||
client: MikrotikClient,
|
||||
args: { peerName: string; serverCertName: string; modeConfigName?: string; comment: string },
|
||||
args: { peerName: string; serverCertName: string; modeConfigName?: string; comment: string; policyTemplateGroup?: string },
|
||||
): Promise<string | undefined> {
|
||||
const identities = await listByPath(client, "/ip/ipsec/identity")
|
||||
const peers = await listByPath(client, "/ip/ipsec/peer")
|
||||
@@ -223,7 +269,13 @@ export async function ensureSharedIke2Identity(
|
||||
identities as Ike2IdentityLike[],
|
||||
peerNames.length > 0 ? peerNames : [args.peerName],
|
||||
)
|
||||
if (existing?.[".id"]) return String(existing[".id"])
|
||||
if (existing?.[".id"]) {
|
||||
if (args.policyTemplateGroup && existing["policy-template-group"] !== args.policyTemplateGroup) {
|
||||
if (!isIpsecManagedComment(existing.comment)) throw new Error("Нельзя изменить группу политик чужой listener identity")
|
||||
await client.patch(`/ip/ipsec/identity/${encodeURIComponent(existing[".id"])}`, { "policy-template-group": args.policyTemplateGroup })
|
||||
}
|
||||
return String(existing[".id"])
|
||||
}
|
||||
|
||||
await putIdentity(client, {
|
||||
peerName: args.peerName,
|
||||
@@ -231,6 +283,7 @@ export async function ensureSharedIke2Identity(
|
||||
comment: args.comment,
|
||||
authMethod: "certificate",
|
||||
certificate: args.serverCertName,
|
||||
policyTemplateGroup: args.policyTemplateGroup,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user