fix(ipsec): исправить статус клиента с полным DN сертификата
This commit is contained in:
1 parent
1d61b50591
commit
2aa93ad365
16 files changed
+1517
-17
No files matched your search
@@ -0,0 +1,18 @@
|
||||
/** RouterOS active-peers.id uses the certificate subject in this field order.
|
||||
* Values are printed verbatim, including commas/plus signs, so parsing the ID
|
||||
* or matching a CN substring would misidentify subjects. Compare the complete
|
||||
* rendered subject and reject collisions in the caller instead.
|
||||
*/
|
||||
export function routerOsCertificateDn(cert: Record<string, string | undefined>): string | undefined {
|
||||
const fields = [["C", "country"], ["S", "state"], ["L", "locality"],
|
||||
["O", "organization"], ["OU", "unit"], ["CN", "common-name"]] as const
|
||||
if (!cert["common-name"]) return undefined
|
||||
const parts: string[] = []
|
||||
for (const [attribute, field] of fields) {
|
||||
const value = cert[field]
|
||||
if (value === undefined || value === "") continue
|
||||
if (typeof value !== "string" || /[\u0000\r\n]/.test(value)) return undefined
|
||||
parts.push(`${attribute}=${value}`)
|
||||
}
|
||||
return parts.join(", ")
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
selectSharedIke2Identity,
|
||||
} from "./ipsec-config.js"
|
||||
import { certificateRole, type CertificateRoleContext } from "./certificate-parse.js"
|
||||
import { routerOsCertificateDn } from "./ipsec-certificate-dn.js"
|
||||
import {
|
||||
canonicalIpsecSnapshot,
|
||||
type IpsecLiveIdentity,
|
||||
@@ -248,10 +249,24 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
|
||||
const sharedRosId = shared?.[".id"] ? String(shared[".id"]) : undefined
|
||||
|
||||
const activeByRemote = new Map<string, RosIpsecActivePeer>()
|
||||
const ambiguousRemoteIds = new Set<string>()
|
||||
for (const a of state.active) {
|
||||
if (a.state !== "established") continue
|
||||
const rid = String(a.id ?? a["remote-id"] ?? "").trim()
|
||||
if (rid) activeByRemote.set(rid, a)
|
||||
if (rid) {
|
||||
if (activeByRemote.has(rid)) ambiguousRemoteIds.add(rid)
|
||||
else activeByRemote.set(rid, a)
|
||||
}
|
||||
}
|
||||
for (const rid of ambiguousRemoteIds) activeByRemote.delete(rid)
|
||||
|
||||
const subjectCounts = new Map<string, number>()
|
||||
const cnCounts = new Map<string, number>()
|
||||
for (const cert of state.certs) {
|
||||
const subject = routerOsCertificateDn(cert)
|
||||
if (subject) subjectCounts.set(subject, (subjectCounts.get(subject) ?? 0) + 1)
|
||||
const cn = cert["common-name"]
|
||||
if (cn) cnCounts.set(cn, (cnCounts.get(cn) ?? 0) + 1)
|
||||
}
|
||||
|
||||
// RouterOS 7 active-peers.id contains the received value without its configured type prefix.
|
||||
@@ -286,10 +301,10 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
|
||||
const comment = identity?.comment ?? ""
|
||||
const mcName = (identity?.["mode-config"] ?? "").trim()
|
||||
const cn = String(cert["common-name"] ?? "").trim()
|
||||
// Our issued certificates have a single CN subject; match the whole DN, never a CN substring.
|
||||
const singleCn = cn && !/[,+=<>;\\"\u0000]/.test(cn) && state.certs.filter(c => c["common-name"] === cn).length === 1
|
||||
? activeByRemote.get(`CN=${cn}`) : undefined
|
||||
const active = (cn ? findActive(cn) : undefined) ?? singleCn
|
||||
const subject = routerOsCertificateDn(cert)
|
||||
const bySubject = subject && subjectCounts.get(subject) === 1 ? activeByRemote.get(subject) : undefined
|
||||
const uniqueCn = cn && cnCounts.get(cn) === 1
|
||||
const active = (uniqueCn ? findActive(cn) : undefined) ?? bySubject
|
||||
?? (identity?.["remote-id"] ? findActive(String(identity["remote-id"]).trim()) : undefined)
|
||||
out.push({
|
||||
id: `${server.id}:${identity ? "identity" : "cert"}:${String(identity?.[".id"] ?? cert[".id"] ?? certName)}`,
|
||||
|
||||
@@ -99,4 +99,34 @@ assert.equal(mapClients(state).find(c => c.certName === "client")?.online, false
|
||||
state.active[0]!.id = "CN=Original CN"
|
||||
state.certs.push({ ".id": "*DUPCN", name: "duplicate-cn", "common-name": "Original CN", ca: "ca", "key-usage": "tls-client" })
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, false); checks++
|
||||
state.certs = state.certs.filter(c => c[".id"] !== "*DUPCN")
|
||||
const certificate = state.certs.find(c => c.name === "client")!
|
||||
Object.assign(certificate, { country: "US", organization: "Example, Ltd", unit: "QA+Operations", "common-name": 'QA, Team+VPN="client"\\Lab' })
|
||||
state.active[0]!.id = 'C=US, O=Example, Ltd, OU=QA+Operations, CN=QA, Team+VPN="client"\\Lab'
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, true); checks++
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.activeAddress, "198.18.79.250"); checks++
|
||||
state.active[0]!.id += ", O=Other"
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, false); checks++
|
||||
state.active[0]!.id = 'C=US, O=Example, Ltd, OU=QA+Operations, CN=QA, Team+VPN="client"\\Lab'
|
||||
// An exact duplicate subject must never be attributed to either certificate.
|
||||
state.certs.push({ ...certificate, ".id": "*DUPSUBJECT", name: "duplicate-subject" })
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, false); checks++
|
||||
state.certs = state.certs.filter(c => c[".id"] !== "*DUPSUBJECT")
|
||||
// Different attribute tuples can have the same unescaped RouterOS rendering.
|
||||
Object.assign(certificate, { organization: "Example, OU=QA", unit: "Ops" })
|
||||
state.active[0]!.id = 'C=US, O=Example, OU=QA, OU=Ops, CN=QA, Team+VPN="client"\\Lab'
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, true); checks++
|
||||
state.certs.push({ ...certificate, ".id": "*AMBIGUOUS", name: "ambiguous-subject", organization: "Example", unit: "QA, OU=Ops" })
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, false); checks++
|
||||
state.certs = state.certs.filter(c => c[".id"] !== "*AMBIGUOUS")
|
||||
Object.assign(certificate, { organization: "Example, Ltd", unit: "QA+Operations" })
|
||||
state.active[0]!.id = 'C=US, O=Example, Ltd, OU=QA+Operations, CN=QA, Team+VPN="client"\\Lab'
|
||||
state.active.push({ ...state.active[0]!, ".id": "*DUPSESSION", "dynamic-address": "198.18.79.251" })
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, false); checks++
|
||||
state.active.pop()
|
||||
Object.assign(certificate, { state: "New York", locality: "Albany" })
|
||||
state.active[0]!.id = 'C=US, S=New York, L=Albany, O=Example, Ltd, OU=QA+Operations, CN=QA, Team+VPN="client"\\Lab'
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, true); checks++
|
||||
certificate.organization = "bad\nsubject"
|
||||
assert.equal(mapClients(state).find(c => c.certName === "client")?.online, false); checks++
|
||||
console.log(`ipsec-state: ${checks} checks passed`)
|
||||
Reference in new issue
Block a user