feat(traffic-flow): add NAT fields to flow processing and analytics
Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m22s
Docker images / frontend-image (push) Successful in 3m30s
Docker images / updater-image (push) Successful in 45s
Docker images / backend-image (push) Successful in 2m51s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 9s

- Introduced new fields for NAT source and destination IPs, as well as their respective ports, in the flow data model.
- Updated database schema and migration scripts to accommodate the new NAT fields in the `flow_buckets` table.
- Enhanced flow analytics and processing functions to utilize the new NAT fields, improving accuracy in traffic flow analysis.
- Added tests to validate the handling of NAT data in various scenarios, ensuring robustness in flow processing.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-11 23:01:48 +07:00
co-authored by Cursor
parent 5f774ce26e
commit fc29dcede7
22 changed files with 578 additions and 85 deletions
+23 -3
View File
@@ -13,6 +13,8 @@ export interface ParsedFlow {
flowEndMs: number
natSrc: string
natDst: string
natSrcPort: number
natDstPort: number
}
export type ParsedFlowInput = Partial<ParsedFlow> & Pick<ParsedFlow, "src" | "dst" | "proto" | "bytes">
@@ -33,6 +35,8 @@ export function emptyParsedFlow(): ParsedFlow {
flowEndMs: 0,
natSrc: "",
natDst: "",
natSrcPort: 0,
natDstPort: 0,
}
}
@@ -45,6 +49,8 @@ export function normalizeParsedFlow(flow: ParsedFlowInput): ParsedFlow {
flowEndMs: flow.flowEndMs ?? 0,
natSrc: flow.natSrc ?? "",
natDst: flow.natDst ?? "",
natSrcPort: flow.natSrcPort ?? 0,
natDstPort: flow.natDstPort ?? 0,
inIface: flow.inIface ?? "",
outIface: flow.outIface ?? "",
srcPort: flow.srcPort ?? 0,
@@ -86,6 +92,12 @@ function ipv4(buf: Buffer, offset: number): string {
return `${buf[offset]}.${buf[offset + 1]}.${buf[offset + 2]}.${buf[offset + 3]}`
}
function usableIpfixIp(ip: string): boolean {
const t = String(ip ?? "").trim()
if (!t) return false
return t !== "0.0.0.0" && t.toLowerCase() !== "::" && t.toLowerCase() !== "::0"
}
function ipv6(buf: Buffer, offset: number): string {
const parts: string[] = []
for (let i = 0; i < 8; i++) parts.push(buf.readUInt16BE(offset + i * 2).toString(16))
@@ -214,6 +226,8 @@ function recordFromFields(
let flowEndMs = 0
let natSrc = ""
let natDst = ""
let natSrcPort = 0
let natDstPort = 0
for (const f of fields) {
const field = consumeField(buf, off, f.length, limit)
if (!field) return null
@@ -243,15 +257,21 @@ function recordFromFields(
case 225:
if (data.length === 4) {
natSrc = ipv4(data, 0)
if (!src) src = natSrc
if (!usableIpfixIp(src) && usableIpfixIp(natSrc)) src = natSrc
}
break
case 226:
if (data.length === 4) {
natDst = ipv4(data, 0)
if (!dst) dst = natDst
if (!usableIpfixIp(dst) && usableIpfixIp(natDst)) dst = natDst
}
break
case 227:
natSrcPort = readUint(data, 0, data.length)
break
case 228:
natDstPort = readUint(data, 0, data.length)
break
case 4:
proto = readUint(data, 0, data.length)
break
@@ -308,7 +328,7 @@ function recordFromFields(
if (ifaceName && !inIface) inIface = ifaceName
return {
flow: normalizeParsedFlow({
src, dst, proto, srcPort, dstPort, bytes, packets, inIface, outIface, nextHop, flowStartMs, flowEndMs, natSrc, natDst,
src, dst, proto, srcPort, dstPort, bytes, packets, inIface, outIface, nextHop, flowStartMs, flowEndMs, natSrc, natDst, natSrcPort, natDstPort,
}),
next: off,
}