fix(gre): отклонять повреждённые и повторяющиеся native идентификаторы

This commit is contained in:
Denozordec committed 2026-10-14 02:09:13 +07:00
1 parent 51a579d370
commit 7a38bb36aa
19 files changed
+678 -19

No files matched your search

+6 -5
View File
@@ -12,7 +12,7 @@ import {
type GreSnapshot,
} from "./entity-snapshots.js"
import { captureAndAppendRevision } from "./config-revisions.js"
import { readGreObservedState } from "./gre-observed-state.js"
import { readGreObservedState, readGreNativeRows } from "./gre-observed-state.js"
type ServerRow = typeof servers.$inferSelect
@@ -114,8 +114,9 @@ export function mapGreLive(
gre: GreLiveIface[]
addrs: GreLiveAddr[]
} {
const observedGre = readGreNativeRows(greRaw)
const addrsByIface = new Map<string, GreLiveAddr[]>()
const greNames = new Set(greRaw.map(g => (g.name ?? "").trim()))
const greNames = new Set(observedGre.map(g => g.name.trim()))
for (const a of addrsRaw) {
if (rosDisabled(a.dynamic)) continue
const iface = (a.interface ?? "").trim()
@@ -134,9 +135,9 @@ export function mapGreLive(
const addrs: GreLiveAddr[] = []
const tunnels: LiveGreTunnel[] = []
greRaw.forEach((g, idx) => {
const rosId = String(g[".id"] ?? g.name ?? `gre-${idx}`)
const name = (g.name ?? "").trim() || `gre-${idx + 1}`
observedGre.forEach((g) => {
const rosId = g[".id"]
const name = g.name.trim()
const keepalive = parseKeepalive(g.keepalive)
const fromComment = parseInnerFromComment(g.comment)
const ifaceAddrs = addrsByIface.get(name) ?? []
@@ -0,0 +1,9 @@
import assert from 'node:assert/strict'
import {readGreNativeRows} from './gre-observed-state.js'
import {mapGreLive} from './gre-live.js'
import {servers} from '../db/schema.js'
const row={'.id':'*A',name:'GRE «тест» / peer',disabled:'false',running:'true'},server={id:1} as typeof servers.$inferSelect
assert.deepEqual(readGreNativeRows([]),[]);assert.equal(mapGreLive(server,[row],[]).tunnels[0].id,'*A');assert.equal(mapGreLive(server,[row],[]).tunnels[0].name,row.name)
const broken=[{},null,[null],[[]],[{...row,'.id':''}],[{...row,name:' '}],[{...row,'.id':undefined}],[{...row,name:undefined}],[{...row,mtu:1400}],[{...row,'.id':' *A'}],[row,row],[row,{...row,'.id':'*B'}],[row,{...row,'.id':'*B',name:' '+row.name+' '}]]
for(const raw of broken){assert.throws(()=>readGreNativeRows(raw),/каталог GRE|native ID/);assert.throws(()=>mapGreLive(server,raw as never,[]),/каталог GRE|native ID/)}
console.log('GRE identity: '+broken.length+' malformed/duplicate cases rejected by raw reader and catalog mapper; empty/native ID/special name preserved')
@@ -1,5 +1,20 @@
import { observedBoolean } from "./ospf-observed-values.js"
/** Native identity must exist before commands or configuration snapshots refer to it. */
export function readGreNativeRows(raw: unknown): Array<Record<string, string>> {
if (!Array.isArray(raw) || raw.some(row => !row || typeof row !== "object" || Array.isArray(row)
|| Object.values(row).some(value => typeof value !== "string")
|| [".id", "name"].some(key => typeof row[key] !== "string" || !row[key].trim())
|| row[".id"] !== row[".id"].trim())) {
throw new Error("Некорректный каталог GRE: отсутствует native ID/имя или повреждены поля")
}
if (new Set(raw.map(row => row[".id"])).size !== raw.length
|| new Set(raw.map(row => row.name.trim())).size !== raw.length) {
throw new Error("Повторяющиеся native ID или имена GRE")
}
return raw as Array<Record<string, string>>
}
/** Disabled is authoritative; an enabled interface also needs a known running flag. */
export function readGreObservedState(flags: { running?: string; disabled?: string }, name: string) {
const disabled = observedBoolean(flags.disabled)
@@ -16,7 +16,7 @@ import {mapGreLive} from './gre-live.js'
import {mapGreAlertRow} from './gre-tunnels-live.js'
let combinations=0
for(const disabled of ['true','yes','false','no',undefined,'','junk'])for(const running of ['true','yes','false','no',undefined,'','junk']){
const row={'.id':'*1',name:'own-flags',disabled,running},off=['true','yes'].includes(disabled??''),on=['false','no'].includes(disabled??''),runningKnown=['true','yes','false','no'].includes(running??'')
const row={'.id':'*1',name:'own-flags',...(disabled===undefined?{}:{disabled}),...(running===undefined?{}:{running})},off=['true','yes'].includes(disabled??''),on=['false','no'].includes(disabled??''),runningKnown=['true','yes','false','no'].includes(running??'')
if(off||on&&runningKnown){const expected=off?'down':['true','yes'].includes(running??'')?'up':'degraded';assert.equal(mapGreAlertRow(devices[0],row).status,expected);assert.equal(mapGreLive(devices[0],[row],[]).tunnels[0].status,expected)}
else {assert.throws(()=>mapGreAlertRow(devices[0],row),/Не подтверждено состояние GRE/);assert.throws(()=>mapGreLive(devices[0],[row],[]),/Не подтверждено состояние GRE/)}
combinations++
+2 -4
View File
@@ -2,8 +2,7 @@ import { eq } from "drizzle-orm"
import { db } from "../db/index.js"
import { servers } from "../db/schema.js"
import { MikrotikClient } from "./mikrotik.js"
import { readInternetPathRows } from "./internet-path-snapshot.js"
import { readGreObservedState } from "./gre-observed-state.js"
import { readGreObservedState, readGreNativeRows } from "./gre-observed-state.js"
type ServerRow = typeof servers.$inferSelect
@@ -40,8 +39,7 @@ export function mapGreAlertRow(server: ServerRow, g: RosGre, idx = 0): GreTunnel
async function fetchGreForServer(server: ServerRow): Promise<GreTunnelLiveRow[]> {
const client = MikrotikClient.fromServer(server)
const greRaw = readInternetPathRows(await client.get<unknown>("/interface/gre"), "GRE alert source", [".id", "name"])
if (new Set(greRaw.map(row => row[".id"])).size !== greRaw.length) throw new Error("Повторяющиеся ID GRE")
const greRaw = readGreNativeRows(await client.get<unknown>("/interface/gre"))
return greRaw.map((g, idx) => mapGreAlertRow(server, g, idx))
}