import type { FlowMapHop } from "@mmapp/contracts/traffic-flow" import { fmtRate } from "@/lib/fmt-rate" export interface MatchedNetflowHop { bps: number bpsFwd: number bpsRev: number bytes: number } function ifaceNorm(s: string | undefined): string { return (s ?? "").trim().toLowerCase() } function pairKey(a: string, b: string): string { const x = String(a) const y = String(b) return x <= y ? `${x}\t${y}` : `${y}\t${x}` } function mergeDirected(hops: FlowMapHop[], mapFromId: string): MatchedNetflowHop { let bytes = 0 let bpsFwd = 0 let bpsRev = 0 const from = String(mapFromId) for (const h of hops) { bytes += h.bytes if (h.fromId === from) { bpsFwd += h.bpsFwd bpsRev += h.bpsRev } else { bpsFwd += h.bpsRev bpsRev += h.bpsFwd } } return { bytes, bpsFwd, bpsRev, bps: bpsFwd + bpsRev } } export function hopHasRate(h: MatchedNetflowHop | undefined): h is MatchedNetflowHop { return h != null && Number.isFinite(h.bps) && h.bps > 0 } export function formatNetflowRate(hop: MatchedNetflowHop): string { return fmtRate(hop.bps / 1_000_000) } export function formatNetflowDir(hop: MatchedNetflowHop): string { return `↓${fmtRate(hop.bpsFwd / 1_000_000)} ↑${fmtRate(hop.bpsRev / 1_000_000)}` } /** GRE: сначала имя интерфейса туннеля на любом конце, иначе пара узлов. */ export function matchNetflowForGreEdge( edge: { tunnel: { name: string } fromServer: { id: string } toServer: { id: string } }, hops: FlowMapHop[], ): MatchedNetflowHop | undefined { const name = ifaceNorm(edge.tunnel.name) const fromId = String(edge.fromServer.id) const toId = String(edge.toServer.id) if (name) { const ifaceHits = hops.filter((h) => h.kind === "iface" && ifaceNorm(h.iface) === name && (h.fromId === fromId || h.fromId === toId), ) if (ifaceHits.length) return mergeDirected(ifaceHits, fromId) const greNamed = hops.filter((h) => h.kind === "gre" && ifaceNorm(h.iface) === name && (h.fromId === fromId || h.fromId === toId || h.toId === fromId || h.toId === toId), ) if (greNamed.length) return mergeDirected(greNamed, fromId) } const want = pairKey(fromId, toId) const pairHits = hops.filter((h) => h.kind === "gre" && Boolean(h.toId) && pairKey(h.fromId, h.toId) === want, ) if (pairHits.length) return mergeDirected(pairHits, fromId) return undefined } /** WAN-аплинк HR: kind wan, иначе iface с тем же именем на homeId. */ export function matchNetflowForWan( homeId: string, wanIface: string, hops: FlowMapHop[], ): MatchedNetflowHop | undefined { const id = String(homeId) const iface = ifaceNorm(wanIface) if (!iface) return undefined const wanHits = hops.filter((h) => h.kind === "wan" && h.fromId === id && ifaceNorm(h.iface) === iface, ) if (wanHits.length) return mergeDirected(wanHits, id) const ifaceHits = hops.filter((h) => h.kind === "iface" && h.fromId === id && ifaceNorm(h.iface) === iface, ) if (ifaceHits.length) return mergeDirected(ifaceHits, id) return undefined }