fix(ospf): показывать IPv6 маршруты и фактический шлюз

This commit is contained in:
Denozordec committed 2026-10-11 23:05:19 +07:00
1 parent 12ead0b6c2
commit f6b9c6941d
19 files changed
+1424 -32

No files matched your search

+10 -3
View File
@@ -17,9 +17,16 @@ for (const rows of [[{ ...row, serverId: 2 }], [row, row], [{ ...row, id: " " }]
assert.equal(result.routes.length, 0)
assert.equal(result.failures.length, 1)
}
const client = { getOspfNeighbors: async () => [], getOspfAreas: async () => [], getOspfInterfaceTemplates: async () => [], getOspfInstances: async () => [], getBfdSessions: async () => [], getIpRoutes: async () => [] }
const client = { getOspfNeighbors: async () => [], getOspfAreas: async () => [], getOspfInterfaceTemplates: async () => [], getOspfInstances: async () => [], getBfdSessions: async () => [], getIpRoutes: async () => [], getIpv6Routes: async () => [] }
assert.equal((await readOspfCatalogRaw(client)).bfdSessions.length, 0)
for (const section of ["getBfdSessions", "getIpRoutes"] as const) await assert.rejects(() => readOspfCatalogRaw({ ...client, [section]: async () => { throw new Error("Отказ " + section) } }), /Отказ/)
for (const section of ["getBfdSessions", "getIpRoutes", "getIpv6Routes"] as const) await assert.rejects(() => readOspfCatalogRaw({ ...client, [section]: async () => { throw new Error("Отказ " + section) } }), /Отказ/)
const dual = await readOspfCatalogRaw({ ...client, getIpRoutes: async () => [{ ".id": "*same", "dst-address": "192.0.2.0/24", ospf: "true" }], getIpv6Routes: async () => [{ ".id": "*same", "dst-address": "fd00:154::/64", ospf: "true", "ospf-metric": "10", "immediate-gw": "fe80::2%gre-own" }] })
assert.deepEqual(dual.ipRoutes.map(r => r[".id"]), ["*same", "ipv6:*same"])
assert.equal(dual.ipRoutes[1]?.["dst-address"], "fd00:154::/64")
const ipv6Catalog = parseCatalogRoutes({ id: 1 } as typeof servers.$inferSelect, dual.ipRoutes)
assert.equal(ipv6Catalog[1]?.via, "gre-own")
assert.equal(ipv6Catalog[1]?.nextHop, "fe80::2")
await assert.rejects(() => readOspfCatalogRaw({ ...client, getIpv6Routes: async () => ({}) as never }), /Некорректный ответ/)
const app = Fastify()
app.setValidatorCompiler(validatorCompiler)
app.setSerializerCompiler(serializerCompiler)
@@ -80,7 +87,7 @@ for (const type of ["home-router", "exit-node"]) {
console.log("OSPF selected: disabled home-router/exit-node do not poll; unknown/invalid IDs fail before RouterOS")
{
const {MikrotikClient}=await import("../services/mikrotik.js"),original=MikrotikClient.fromServer
MikrotikClient.fromServer=()=>({getOspfNeighbors:async()=>[],getOspfAreas:async()=>[],getOspfInterfaceTemplates:async()=>[],getOspfInstances:async()=>[],getBfdSessions:async()=>{throw Error("BFD unavailable")},getIpRoutes:async()=>[]} as unknown as MikrotikClient)
MikrotikClient.fromServer=()=>({getOspfNeighbors:async()=>[],getOspfAreas:async()=>[],getOspfInterfaceTemplates:async()=>[],getOspfInstances:async()=>[],getBfdSessions:async()=>{throw Error("BFD unavailable")},getIpRoutes:async()=>[],getIpv6Routes:async()=>[]} as unknown as MikrotikClient)
const testApp=Fastify();testApp.setValidatorCompiler(validatorCompiler);testApp.setSerializerCompiler(serializerCompiler)
try{await testApp.register(createOspfRoutes({listEnabledServers:async()=>[],readCatalog:async()=>emptyOspfCatalog(),findServer:async()=>({id:1,name:"test",enabled:true} as typeof servers.$inferSelect)}),{prefix:"/api"});const r=await testApp.inject({method:"GET",url:"/api/servers/1/ospf"});assert.equal(r.statusCode,503);assert.match(r.json().error,/BFD unavailable/)}finally{await testApp.close();MikrotikClient.fromServer=original}
}
+1 -10
View File
@@ -80,16 +80,7 @@ function parseAddrIface(addr: string): { ip: string; iface: string } {
/** Fetch all OSPF + BFD data for one server */
async function fetchServerOspf(server: ServerRow) {
const client = MikrotikClient.fromServer(server)
const [neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes] = await Promise.all([
client.getOspfNeighbors(),
client.getOspfAreas(),
client.getOspfInterfaceTemplates(),
client.getOspfInstances(),
client.getBfdSessions().catch(() => [] as RosBfdSession[]), // BFD is optional
client.getIpRoutes().catch(() => [] as RosIpRoute[]),
])
return { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes }
return readOspfCatalogRaw(MikrotikClient.fromServer(server))
}
// ── BFD parser ────────────────────────────────────────────────────────────────
+4
View File
@@ -683,6 +683,10 @@ export class MikrotikClient {
return this.get<RosIpRoute[]>("/ip/route")
}
async getIpv6Routes(): Promise<RosIpRoute[]> {
return this.get<RosIpRoute[]>("/ipv6/route")
}
async getFirewallFilters(): Promise<RosFirewallFilter[]> {
return this.getFirewallRules("ip", "filter")
}
+5 -4
View File
@@ -11,11 +11,12 @@ export interface OspfCatalogCollections {
bfdSessions: CatalogBfdSession[]; routes: CatalogRoute[];
}
export const emptyOspfCatalog = (): OspfCatalogCollections => ({ neighbors: [], interfaces: [], instances: [], bfdSessions: [], routes: [] })
export async function readOspfCatalogRaw(client: Pick<MikrotikClient, "getOspfNeighbors" | "getOspfAreas" | "getOspfInterfaceTemplates" | "getOspfInstances" | "getBfdSessions" | "getIpRoutes">) {
const [neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes] = await Promise.all([
client.getOspfNeighbors(), client.getOspfAreas(), client.getOspfInterfaceTemplates(), client.getOspfInstances(), client.getBfdSessions(), client.getIpRoutes(),
export async function readOspfCatalogRaw(client: Pick<MikrotikClient, "getOspfNeighbors" | "getOspfAreas" | "getOspfInterfaceTemplates" | "getOspfInstances" | "getBfdSessions" | "getIpRoutes" | "getIpv6Routes">) {
const [neighbors, areas, ifaceTemplates, instances, bfdSessions, v4Routes, v6Routes] = await Promise.all([
client.getOspfNeighbors(), client.getOspfAreas(), client.getOspfInterfaceTemplates(), client.getOspfInstances(), client.getBfdSessions(), client.getIpRoutes(), client.getIpv6Routes(),
])
if ([neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes].some(rows => !Array.isArray(rows))) throw new Error("Некорректный ответ RouterOS OSPF")
if ([neighbors, areas, ifaceTemplates, instances, bfdSessions, v4Routes, v6Routes].some(rows => !Array.isArray(rows))) throw new Error("Некорректный ответ RouterOS OSPF")
const ipRoutes = [...v4Routes, ...v6Routes.map((route, index) => ({ ...route, ".id": `ipv6:${route[".id"] ?? index}` }))]
return { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes }
}
export async function pollOspfCatalog<S extends { id: number; name: string }>(devices: readonly S[], read: (device: S) => Promise<OspfCatalogCollections>) {
@@ -20,6 +20,9 @@ assert.deepEqual(parseOspfGateway(route({ gateway: "10.200.0.1%gre-msk-spb" })),
})
console.log("ospf-route-parse.test.ts: ok")
assert.deepEqual(parseOspfGateway(route({ gateway: "configured-gateway", "immediate-gw": "fe80::2%gre-own" })), { nextHop: "fe80::2", via: "gre-own" })
assert.deepEqual(parseOspfGateway(route({ gateway: "", "immediate-gw": "198.18.154.2%gre-own" })), { nextHop: "198.18.154.2", via: "gre-own" })
assert.deepEqual(parseOspfGateway(route({ gateway: "198.18.154.2%gre-own", "immediate-gw": "" })), { nextHop: "198.18.154.2", via: "gre-own" })
assert.equal(parseOspfCatalogRouteType(route({ ospf: "true" })), "Unknown")
assert.equal(parseOspfCatalogRouteType(route({ ospf: "true", "ospf-type": "future-type" })), "Unknown")
+1 -1
View File
@@ -19,7 +19,7 @@ export function parseOspfRouteType(r: RosIpRoute): OspfRouteKind | null {
}
export function parseOspfGateway(r: RosIpRoute): { nextHop: string; via: string } {
const gw = (r.gateway ?? r["immediate-gw"] ?? "").trim()
const gw = r["immediate-gw"]?.trim() || r.gateway?.trim() || ""
const [ip, iface = ""] = gw.split("%")
return {
nextHop: ip || gw || "—",