Files
MikrotikManager/backend/src/services/traffic-flow-planes.ts
T
DenozordecandCursor 90c8c393e5
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Failing after 1m31s
Docker images / frontend-image (push) Successful in 2m36s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 42s
Docker images / publish-release (push) Skipped
feat(traffic): разделить плоскости IPFIX и показать путь JH→EN
Считать payload отдельно от overlay GRE/ESP и mesh; вкладка Пути и KPI Wire из счётчиков iface.

Co-authored-by: Cursor <[email protected]>
2026-09-07 11:39:14 +07:00

108 lines
3.7 KiB
TypeScript

export type FlowPlane = "payload" | "client_mesh" | "overlay" | "mgmt"
export const PLANE_LABEL: Record<FlowPlane, string> = {
payload: "Интернет",
client_mesh: "Клиенты",
overlay: "JH↔EN",
mgmt: "mgmt",
}
const WG_PORTS = new Set([51820, 13232, 51821])
const FLOW_PORTS = new Set([4739, 2055])
export function isRfc1918(ip: string): boolean {
const parts = String(ip ?? "").split(".").map((n) => Number.parseInt(n, 10))
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return false
const [a, b] = parts
if (a === 10) return true
if (a === 192 && b === 168) return true
if (a === 172 && b != null && b >= 16 && b <= 31) return true
if (a === 100 && b != null && b >= 64 && b <= 127) return true
return false
}
export function isPublicV4(ip: string): boolean {
const parts = String(ip ?? "").split(".").map((n) => Number.parseInt(n, 10))
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return false
const a = parts[0] ?? 0
if (a === 0 || a === 127 || a >= 224) return false
return !isRfc1918(ip)
}
export function isTunnelProto(proto: number, srcPort: number, dstPort: number): boolean {
if (proto === 47 || proto === 50) return true
if (proto === 17 && (WG_PORTS.has(srcPort) || WG_PORTS.has(dstPort))) return true
return false
}
function ifaceLooksMgmt(name: string): boolean {
const n = name.trim().toLowerCase()
return n === "wg-flow" || n.endsWith("/wg-flow") || n.includes("wg-flow")
}
export interface PlaneFlowInput {
src: string
dst: string
proto: number
srcPort: number
dstPort: number
inIface: string
outIface?: string
}
/** Быстрая классификация без топологии — для live ring на ingest. */
export function classifyFlowPlaneLite(flow: PlaneFlowInput): FlowPlane {
if (ifaceLooksMgmt(flow.inIface)) return "mgmt"
if (flow.proto === 17 && (FLOW_PORTS.has(flow.srcPort) || FLOW_PORTS.has(flow.dstPort))) return "mgmt"
if (isTunnelProto(flow.proto, flow.srcPort, flow.dstPort)) return "overlay"
if (isRfc1918(flow.src) && isRfc1918(flow.dst)) return "client_mesh"
return "payload"
}
export interface PlaneTopology {
clientIfaceNames: Set<string>
enHosts: Set<string>
jhHosts: Set<string>
}
function hostHit(ip: string, hosts: Set<string>): boolean {
return Boolean(ip) && hosts.has(ip)
}
export function classifyFlowPlane(
flow: PlaneFlowInput,
topo?: PlaneTopology | null,
): FlowPlane {
const lite = classifyFlowPlaneLite(flow)
if (!topo) return lite
if (lite === "mgmt") return "mgmt"
if (lite === "overlay") return "overlay"
const srcEn = hostHit(flow.src, topo.enHosts) || hostHit(flow.src, topo.jhHosts)
const dstEn = hostHit(flow.dst, topo.enHosts) || hostHit(flow.dst, topo.jhHosts)
if (srcEn && dstEn && isPublicV4(flow.src) && isPublicV4(flow.dst)) return "overlay"
if (lite === "client_mesh") {
const inClient = topo.clientIfaceNames.has(flow.inIface)
const outClient = Boolean(flow.outIface && topo.clientIfaceNames.has(flow.outIface))
if (inClient || outClient || (isRfc1918(flow.src) && isRfc1918(flow.dst))) return "client_mesh"
}
return "payload"
}
export function shouldKeepPlane(
plane: FlowPlane,
opts: { excludeMesh?: boolean; excludeOverlay?: boolean },
): boolean {
if (plane === "mgmt") return false
if (opts.excludeMesh !== false && plane === "client_mesh") return false
if (opts.excludeOverlay !== false && plane === "overlay") return false
return true
}
export function flowBps(bytes: number, startMs: number, endMs: number, windowSec: number): number {
if (startMs > 0 && endMs > startMs) {
const sec = Math.max(1, (endMs - startMs) / 1000)
return (bytes * 8) / sec
}
return (bytes * 8) / Math.max(1, windowSec)
}