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]>
142 lines
4.8 KiB
TypeScript
142 lines
4.8 KiB
TypeScript
/** 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
|
||
}
|