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
Считать payload отдельно от overlay GRE/ESP и mesh; вкладка Пути и KPI Wire из счётчиков iface. Co-authored-by: Cursor <[email protected]>
108 lines
3.7 KiB
TypeScript
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)
|
|
}
|