feat: NetFlow/IPFIX traffic analytics — per-client usage, app breakdown, built-in collector (v0.13.0-beta)
Adds Meraki-style traffic analytics powered by MikroTik Traffic Flow: Backend - Built-in NetFlow v9 + IPFIX collector (UDP 2055) with hand-rolled decoder, template caching, enterprise-field and IPv6 support - Flow attribution: client IP → MAC via the clients table, upload/download direction, NAT-duplicate records dropped - App classification by protocol/port (~25 categories: HTTPS, QUIC, DNS, SSH, Email, WireGuard, ...) - Deduplication: per-client best-exporter-per-window so flows crossing two managed routers are never double-counted - NAT-tolerant ingest: flows from sources matching no managed device (NAT / Tailscale subnet routers in path) are accepted as per-source unidentified exporters instead of silently dropped (netflow_accept_unknown setting) - Storage: InfluxDB client_traffic time-series (bounded cardinality via top-N fold) + Postgres client_traffic_daily rollups; daily retention purge - One-toggle device config push via RouterOS API (/ip traffic-flow + target) - New /api/traffic/* analytics endpoints + /api/network-services/netflow - docker-compose: NETFLOW_PORT (default 2055/udp) published on the backend Frontend - New Traffic page: fleet up/down chart, top talkers with client click-through, app-category donut, 1h–30d ranges - New NetFlow config page: collector settings, live ingest stats, per-device export toggles with health badges, NAT-source banner - Client detail: App Traffic card (auto-hides without data) - Clients page: sortable "Data (today)" column - Settings: NetFlow retention controls Includes 17 unit tests (decoder fixtures, dedup, attribution, classifier); verified end-to-end against live RouterOS devices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
367034c2d6
commit
b841eb6e7b
@@ -775,6 +775,73 @@ export const networkServicesApi = {
|
||||
api.put<NS[]>(`/network-services/wireguard/peer/${encodeURIComponent(id)}`, body, { params: { deviceId } }),
|
||||
deleteWireGuardPeer: (deviceId: number, id: string) =>
|
||||
api.delete(`/network-services/wireguard/peer/${encodeURIComponent(id)}`, { params: { deviceId } }),
|
||||
|
||||
// ── NetFlow ───────────────────────────────────────────────────────────────
|
||||
netflowFleet: () =>
|
||||
api.get<{
|
||||
collector: { address: string; port: number; version: string; listening: boolean };
|
||||
devices: NetflowDeviceState[];
|
||||
}>('/network-services/netflow/fleet', { timeout: 60_000 }),
|
||||
getNetflow: (deviceId: number) =>
|
||||
api.get<{ settings: NS | null; targets: NS[]; target_matches_collector: boolean }>(
|
||||
'/network-services/netflow', { params: { deviceId } }
|
||||
),
|
||||
setNetflow: (deviceId: number, enabled: boolean) =>
|
||||
api.put<{ settings: NS | null; targets: NS[]; target_matches_collector: boolean }>(
|
||||
'/network-services/netflow', { enabled }, { params: { deviceId } }
|
||||
),
|
||||
};
|
||||
|
||||
export interface NetflowDeviceState {
|
||||
id: number;
|
||||
name: string;
|
||||
ip_address: string;
|
||||
enabled: boolean | null;
|
||||
interfaces: string;
|
||||
targets: { id: string; dst_address: string; port: number; version: string; disabled: boolean }[];
|
||||
target_matches_collector: boolean;
|
||||
flows_received: number;
|
||||
last_flow_at: string | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ─── Traffic Analytics (NetFlow) ──────────────────────────────────────────────
|
||||
|
||||
export interface ClientTrafficPoint { time: string; upload: number; download: number }
|
||||
export interface TrafficTopClient {
|
||||
mac: string;
|
||||
upload_bytes: number;
|
||||
download_bytes: number;
|
||||
total_bytes: number;
|
||||
hostname: string | null;
|
||||
custom_name: string | null;
|
||||
vendor: string | null;
|
||||
ip_address: string | null;
|
||||
}
|
||||
export interface TrafficApp { app: string; bytes: number }
|
||||
export interface TrafficCollectorStats {
|
||||
listening: boolean;
|
||||
port: number;
|
||||
packetsReceived: number;
|
||||
flowsDecoded: number;
|
||||
flowsAttributed: number;
|
||||
packetsFromUnknownExporter: number;
|
||||
recordsWithoutTemplate: number;
|
||||
exporters: { deviceId: number; deviceName: string; packets: number; flows: number; lastSeen: string | null }[];
|
||||
}
|
||||
|
||||
export const trafficApi = {
|
||||
status: () => api.get<TrafficCollectorStats>('/traffic/status'),
|
||||
timeseries: (range = '24h') =>
|
||||
api.get<ClientTrafficPoint[]>('/traffic/timeseries', { params: { range } }),
|
||||
topClients: (range = '24h', limit = 10) =>
|
||||
api.get<TrafficTopClient[]>('/traffic/top-clients', { params: { range, limit } }),
|
||||
apps: (range = '24h', mac?: string) =>
|
||||
api.get<TrafficApp[]>('/traffic/apps', { params: { range, mac } }),
|
||||
client: (mac: string, range = '24h') =>
|
||||
api.get<{ series: ClientTrafficPoint[]; apps: TrafficApp[] }>(
|
||||
`/traffic/client/${encodeURIComponent(mac)}`, { params: { range } }
|
||||
),
|
||||
};
|
||||
|
||||
// ─── Maintenance Windows ──────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user