Files
MikrotikManager/backend/src/services/traffic-flow-ip.ts
T
DenozordecandCursor fc29dcede7
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
feat(traffic-flow): add NAT fields to flow processing and analytics
- 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]>
2026-09-11 23:01:48 +07:00

142 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** IPv4 helpers for RIPEstat prefix cache and EvoBGP CIDR match. */
export function ipv4ToInt(ip: string): number | null {
const parts = String(ip ?? "").trim().split(".")
if (parts.length !== 4) return null
let n = 0
for (const p of parts) {
if (!/^\d+$/.test(p)) return null
const o = Number(p)
if (o < 0 || o > 255) return null
n = ((n << 8) >>> 0) + o
}
return n >>> 0
}
export function parseCidrV4(cidr: string): { net: number; mask: number; prefixLen: number } | null {
const raw = String(cidr ?? "").trim()
const [ip, lenRaw] = raw.split("/")
const addr = ipv4ToInt(ip ?? "")
const prefixLen = Number.parseInt(lenRaw ?? "", 10)
if (addr == null || !Number.isFinite(prefixLen) || prefixLen < 0 || prefixLen > 32) return null
const mask = prefixLen === 0 ? 0 : (0xffffffff << (32 - prefixLen)) >>> 0
return { net: (addr & mask) >>> 0, mask, prefixLen }
}
export function ipInCidrV4(ip: string, cidr: string): boolean {
const addr = ipv4ToInt(ip)
const parsed = parseCidrV4(cidr)
if (addr == null || !parsed) return false
return ((addr & parsed.mask) >>> 0) === parsed.net
}
export function isNonPublicIp(ip: string): boolean {
const trimmed = String(ip ?? "").trim()
if (!trimmed) return true
if (trimmed.includes(":")) {
const lower = trimmed.toLowerCase()
return lower === "::1" || lower.startsWith("fe80:") || lower.startsWith("fc") || lower.startsWith("fd") || lower === "::"
}
const n = ipv4ToInt(trimmed)
if (n == null) return true
const inRange = (cidr: string) => ipInCidrV4(trimmed, cidr)
return (
inRange("0.0.0.0/8")
|| inRange("10.0.0.0/8")
|| inRange("127.0.0.0/8")
|| inRange("169.254.0.0/16")
|| inRange("172.16.0.0/12")
|| inRange("192.168.0.0/16")
|| inRange("100.64.0.0/10")
|| inRange("224.0.0.0/4")
|| inRange("255.255.255.255/32")
)
}
const PEER_WELL_KNOWN_PORTS = new Set([80, 443, 53, 853])
export function isUnspecifiedIp(ip: string): boolean {
const t = String(ip ?? "").trim()
if (!t) return true
const lower = t.toLowerCase()
return t === "0.0.0.0" || lower === "::" || lower === "::0"
}
function usableIp(ip: string | undefined): string {
const t = String(ip ?? "").trim()
return isUnspecifiedIp(t) ? "" : t
}
export interface InternetDestCtx {
/** WAN IP узлов сети (EN/JH) — не интернет-назначение. */
ours?: ReadonlySet<string>
/** Ingress с bound GRE/WG клиента: dest = нелокальный IP, не ASN клиента. */
boundClient?: boolean
/** IPFIX postNAT (IANA 225/226). */
natSrc?: string
natDst?: string
/** IPFIX postNAPT ports (IANA 227/228). */
natSrcPort?: number
natDstPort?: number
}
export function isLocalIp(ip: string, ours?: ReadonlySet<string>): boolean {
if (isUnspecifiedIp(ip) || isNonPublicIp(ip)) return true
return Boolean(ours?.has(String(ip ?? "").trim()))
}
/**
* Интернет-назначение потока для ASN/страны/сервиса.
* Пустая строка — dest нет (не GeoIP IP клиента / GRE-пира).
*/
export function pickInternetDest(
srcRaw: string,
dstRaw: string,
srcPort: number,
dstPort: number,
ctx?: InternetDestCtx,
): string {
const ours = ctx?.ours
const src = usableIp(srcRaw)
const dst = usableIp(dstRaw)
const natSrc = usableIp(ctx?.natSrc)
const natDst = usableIp(ctx?.natDst)
const internet = (ip: string) => Boolean(ip) && !isLocalIp(ip, ours)
const dstIp = internet(dst) ? dst : (internet(natDst) ? natDst : "")
const srcIp = internet(src) ? src : (internet(natSrc) ? natSrc : "")
const dstPortEff = internet(dst) ? dstPort : (internet(natDst) ? (ctx?.natDstPort || dstPort) : dstPort)
const srcPortEff = internet(src) ? srcPort : (internet(natSrc) ? (ctx?.natSrcPort || srcPort) : srcPort)
if (ctx?.boundClient) {
if (dstIp) return dstIp
if (srcIp) {
const srcWk = PEER_WELL_KNOWN_PORTS.has(srcPortEff)
const dstWk = PEER_WELL_KNOWN_PORTS.has(dstPortEff)
if (srcWk && !dstWk) return srcIp
return ""
}
return ""
}
if (srcIp && !dstIp) return srcIp
if (dstIp && !srcIp) return dstIp
if (srcIp && dstIp) {
const srcWk = PEER_WELL_KNOWN_PORTS.has(srcPortEff)
const dstWk = PEER_WELL_KNOWN_PORTS.has(dstPortEff)
if (srcWk && !dstWk) return srcIp
if (dstWk && !srcWk) return dstIp
return dstIp
}
if (src && dst && ours?.has(src) && ours.has(dst)) return ""
return dst
}
/**
* Интернет-сторона потока без топологии: у IPFIX сервис часто в src (Google:443 → RFC1918).
* Для куба статистики используйте pickInternetDest.
*/
export function pickInternetPeer(src: string, dst: string, srcPort: number, dstPort: number): string {
return pickInternetDest(src, dst, srcPort, dstPort) || dst
}