fix(ipsec): учитывать уникальный DNS SAN в активных сессиях
This commit is contained in:
1 parent
b4e56d1998
commit
d4f5746325
12 files changed
+308
-19
No files matched your search
@@ -261,11 +261,18 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
|
||||
|
||||
const subjectCounts = new Map<string, number>()
|
||||
const cnCounts = new Map<string, number>()
|
||||
// RouterOS reports the received ID without its type. Only exact DNS SANs
|
||||
// are usable here; wildcards and other SAN namespaces are not IKE aliases.
|
||||
const dnsSans = (cert: RosCertRow) => [...new Set((cert["subject-alt-name"] ?? "").split(",")
|
||||
.map(value => value.trim()).filter(value => /^DNS:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(value))
|
||||
.map(value => value.slice(4)))]
|
||||
const sanCounts = 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)
|
||||
for (const san of dnsSans(cert)) sanCounts.set(san, (sanCounts.get(san) ?? 0) + 1)
|
||||
}
|
||||
|
||||
// RouterOS 7 active-peers.id contains the received value without its configured type prefix.
|
||||
@@ -285,12 +292,14 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
|
||||
}
|
||||
|
||||
const out: IpsecClientDto[] = []
|
||||
const sessionClaims = new Map<RosIpsecActivePeer[], IpsecClientDto[]>()
|
||||
const sessionClaims = new Map<RosIpsecActivePeer, IpsecClientDto[]>()
|
||||
const claimSession = (active: RosIpsecActivePeer[] | undefined) => {
|
||||
if (!active) return
|
||||
const claims = sessionClaims.get(active) ?? []
|
||||
claims.push(out[out.length - 1]!)
|
||||
sessionClaims.set(active, claims)
|
||||
for (const session of active) {
|
||||
const claims = sessionClaims.get(session) ?? []
|
||||
claims.push(out[out.length - 1]!)
|
||||
sessionClaims.set(session, claims)
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Клиентские сертификаты (роль client), подписанные нашим CA / выданные менеджером / referenced identity.
|
||||
@@ -314,8 +323,18 @@ export function mapClients(state: IpsecServerState): IpsecClientDto[] {
|
||||
// A literal CN is not a typed remote-id. It may also resemble another
|
||||
// certificate's complete DN, in which case the bare-CN alias is unsafe.
|
||||
const byCommonName = uniqueCn && !subjectCounts.has(cn) ? activeByRemote.get(cn) : undefined
|
||||
const active = bySubject ?? byCommonName
|
||||
?? (identity?.["remote-id"] ? findActive(String(identity["remote-id"]).trim()) : undefined)
|
||||
const remoteId = (identity?.["remote-id"] ?? "").trim()
|
||||
// SAN inference requires a directly bound certificate and no explicit ID.
|
||||
// Count every certificate, including non-catalog certificates, to avoid
|
||||
// attributing a shared alias. DN/CN namespaces take precedence over SANs.
|
||||
const bySan = identity && (!remoteId || remoteId === "auto")
|
||||
? dnsSans(cert).filter(san => sanCounts.get(san) === 1 && !subjectCounts.has(san) && !cnCounts.has(san))
|
||||
.flatMap(san => activeByRemote.get(san) ?? []) : []
|
||||
const sessions = [...new Set([
|
||||
...(bySubject ?? []), ...(byCommonName ?? []), ...bySan,
|
||||
...(remoteId ? findActive(remoteId) ?? [] : []),
|
||||
])]
|
||||
const active = sessions.length ? sessions : undefined
|
||||
// Multiple established sessions confirm presence, but no single lease/uptime.
|
||||
const single = active?.length === 1 ? active[0] : undefined
|
||||
out.push({
|
||||
|
||||
@@ -213,4 +213,35 @@ sharedCertState.active[0]!.id="client0.example"
|
||||
const distinctRows=mapClients(sharedCertState)
|
||||
assert.equal(distinctRows.filter(c => c.online).length,1)
|
||||
assert(distinctRows.every(c => !c.sessionAmbiguous)); checks++
|
||||
// A real certificate-authenticated FQDN SAN session may omit remote-id.
|
||||
const sanState = { ...sharedCertState, certs: [{ ...sharedCertState.certs[0]!, "subject-alt-name": "DNS:vpn.example,DNS:other.example,DNS:vpn.example" }], identities: [{ ...sharedCertState.identities[0]!, "remote-id": "" }], active: [{ ".id": "*SAN", id: "vpn.example", state: "established", "dynamic-address": "198.18.121.250", uptime: "1m" }] } as typeof state
|
||||
assert.equal(mapClients(sanState)[0]?.online,true)
|
||||
assert.equal(mapClients(sanState)[0]?.activeAddress,"198.18.121.250"); checks++
|
||||
sanState.active.push({ ...sanState.active[0]!, ".id": "*DN", id: "CN=alice", "dynamic-address": "198.18.121.251" })
|
||||
for (const reverse of [false,true]) {
|
||||
if(reverse) sanState.active.reverse()
|
||||
const row=mapClients(sanState)[0]!
|
||||
assert(row.online && !row.sessionAmbiguous && !row.activeAddress && !row.activeSince); checks++
|
||||
}
|
||||
sanState.active = sanState.active.filter(a=>a.id==="vpn.example")
|
||||
sanState.certs.push({ ...sanState.certs[0]!, ".id":"*FOREIGN", name:"foreign", ca:"other", "key-usage":"tls-server" })
|
||||
assert.equal(mapClients(sanState)[0]?.online,false); checks++
|
||||
sanState.certs.pop()
|
||||
sanState.identities[0]!["remote-id"]="fqdn:unrelated.example"
|
||||
assert.equal(mapClients(sanState)[0]?.online,false); checks++
|
||||
sanState.identities[0]!["remote-id"]="auto"
|
||||
assert.equal(mapClients(sanState)[0]?.online,true); checks++
|
||||
sanState.identities.push({ ".id":"*PSK-SAN", peer:"ike", "auth-method":"pre-shared-key", "remote-id":"key-id:vpn.example", comment:ipsecManagedComment("user=collision") })
|
||||
assert(mapClients(sanState).every(c=>c.sessionAmbiguous && !c.online && !c.activeAddress && !c.activeSince)); checks++
|
||||
sanState.identities.pop()
|
||||
for(const alias of ["DNS:*.example", "IP:198.18.121.250", "email:vpn.example", "DNS:bad\n.example"]) {
|
||||
sanState.certs[0]!["subject-alt-name"]=alias
|
||||
assert.equal(mapClients(sanState)[0]?.online,false); checks++
|
||||
}
|
||||
sanState.certs[0]!["subject-alt-name"]="DNS:vpn.example"
|
||||
sanState.certs.push({ ".id":"*CN-SHADOW",name:"foreign-cn", "common-name":"vpn.example" })
|
||||
assert.equal(mapClients(sanState)[0]?.online,false); checks++
|
||||
sanState.certs.pop()
|
||||
sanState.identities=[]
|
||||
assert.equal(mapClients(sanState)[0]?.online,false); checks++
|
||||
console.log(`ipsec-state: ${checks} checks passed`)
|
||||
Reference in new issue
Block a user