Docker images / prepare-release (push) Successful in 11s
Docker images / backend-test (push) Successful in 2m5s
Docker images / frontend-image (push) Successful in 3m14s
Docker images / updater-image (push) Successful in 46s
Docker images / backend-image (push) Successful in 4m2s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
В аналитике потока видны сети со своим AS (Apple, Steam, VK, Epic и другие), а не только Cloudflare/AWS. Holder и порты Steam не перебивают Cloudflare CIDR. Co-authored-by: Cursor <[email protected]>
310 lines
12 KiB
TypeScript
310 lines
12 KiB
TypeScript
import { ipInCidrV4, parseCidrV4 } from "./traffic-flow-ip.js"
|
|
|
|
export const OTHER_SERVICE = "Прочее"
|
|
|
|
export interface BrandHit {
|
|
service: string
|
|
category: string
|
|
}
|
|
|
|
const CDN = { category: "CDN" } as const
|
|
const WEB = { category: "Веб" } as const
|
|
const VIDEO = { category: "Видео / стриминг" } as const
|
|
const GAMES = { category: "Игры" } as const
|
|
const VOICE = { category: "Голос" } as const
|
|
const AI = { category: "ИИ" } as const
|
|
const DNS = { category: "DNS" } as const
|
|
|
|
const CLOUDFLARE: BrandHit = { service: "Cloudflare", ...CDN }
|
|
const FASTLY: BrandHit = { service: "Fastly", ...CDN }
|
|
const AKAMAI: BrandHit = { service: "Akamai", ...CDN }
|
|
const AWS: BrandHit = { service: "AWS", ...CDN }
|
|
const MICROSOFT: BrandHit = { service: "Microsoft", ...CDN }
|
|
const YANDEX: BrandHit = { service: "Yandex", ...CDN }
|
|
const APPLE: BrandHit = { service: "Apple", ...CDN }
|
|
const DIGITALOCEAN: BrandHit = { service: "DigitalOcean", ...CDN }
|
|
const HETZNER: BrandHit = { service: "Hetzner", ...CDN }
|
|
const OVH: BrandHit = { service: "OVH", ...CDN }
|
|
const ORACLE: BrandHit = { service: "Oracle", ...CDN }
|
|
const LINODE: BrandHit = { service: "Linode", ...CDN }
|
|
const VULTR: BrandHit = { service: "Vultr", ...CDN }
|
|
const SCALEWAY: BrandHit = { service: "Scaleway", ...CDN }
|
|
const IBM_CLOUD: BrandHit = { service: "IBM Cloud", ...CDN }
|
|
const ALIBABA: BrandHit = { service: "Alibaba", ...CDN }
|
|
const TENCENT: BrandHit = { service: "Tencent", ...CDN }
|
|
const GCORE: BrandHit = { service: "G-Core", ...CDN }
|
|
const CDN77: BrandHit = { service: "CDN77", ...CDN }
|
|
const SELECTEL: BrandHit = { service: "Selectel", ...CDN }
|
|
const TIMEWEB: BrandHit = { service: "Timeweb", ...CDN }
|
|
const BEGET: BrandHit = { service: "Beget", ...CDN }
|
|
const DDOS_GUARD: BrandHit = { service: "DDoS-Guard", ...CDN }
|
|
const META: BrandHit = { service: "Meta", ...CDN }
|
|
|
|
const GOOGLE: BrandHit = { service: "Google", ...WEB }
|
|
const GITHUB: BrandHit = { service: "GitHub", ...WEB }
|
|
const GITLAB: BrandHit = { service: "GitLab", ...WEB }
|
|
const X: BrandHit = { service: "X", ...WEB }
|
|
const LINKEDIN: BrandHit = { service: "LinkedIn", ...WEB }
|
|
const VK: BrandHit = { service: "VK", ...WEB }
|
|
const REDDIT: BrandHit = { service: "Reddit", ...WEB }
|
|
const DROPBOX: BrandHit = { service: "Dropbox", ...WEB }
|
|
const SNAP: BrandHit = { service: "Snap", ...WEB }
|
|
const WIKIPEDIA: BrandHit = { service: "Wikipedia", ...WEB }
|
|
const PAYPAL: BrandHit = { service: "PayPal", ...WEB }
|
|
const SALESFORCE: BrandHit = { service: "Salesforce", ...WEB }
|
|
|
|
const YOUTUBE: BrandHit = { service: "YouTube", ...VIDEO }
|
|
const NETFLIX: BrandHit = { service: "Netflix", ...VIDEO }
|
|
const TWITCH: BrandHit = { service: "Twitch", ...VIDEO }
|
|
const TIKTOK: BrandHit = { service: "TikTok", ...VIDEO }
|
|
const SPOTIFY: BrandHit = { service: "Spotify", ...VIDEO }
|
|
|
|
const STEAM: BrandHit = { service: "Steam", ...GAMES }
|
|
const BLIZZARD: BrandHit = { service: "Blizzard", ...GAMES }
|
|
const EPIC: BrandHit = { service: "Epic", ...GAMES }
|
|
const RIOT: BrandHit = { service: "Riot", ...GAMES }
|
|
const PLAYSTATION: BrandHit = { service: "PlayStation", ...GAMES }
|
|
const ROBLOX: BrandHit = { service: "Roblox", ...GAMES }
|
|
const UBISOFT: BrandHit = { service: "Ubisoft", ...GAMES }
|
|
|
|
const DISCORD: BrandHit = { service: "Discord", ...VOICE }
|
|
const TELEGRAM: BrandHit = { service: "Telegram", ...VOICE }
|
|
const ZOOM: BrandHit = { service: "Zoom", ...VOICE }
|
|
|
|
const CHATGPT: BrandHit = { service: "ChatGPT", ...AI }
|
|
|
|
const QUAD9: BrandHit = { service: "Quad9", ...DNS }
|
|
const OPENDNS: BrandHit = { service: "OpenDNS", ...DNS }
|
|
|
|
function brandEntries(hit: BrandHit, asns: number[]): Array<[number, BrandHit]> {
|
|
return asns.map((asn) => [asn, hit])
|
|
}
|
|
|
|
function hqEntries(cc: string, asns: number[]): Array<[number, string]> {
|
|
return asns.map((asn) => [asn, cc])
|
|
}
|
|
|
|
const ASN_BRANDS = new Map<number, BrandHit>([
|
|
...brandEntries(CLOUDFLARE, [13335, 209242]),
|
|
...brandEntries(FASTLY, [54113]),
|
|
...brandEntries(AKAMAI, [20940, 16625, 32787, 35994, 16702, 24319]),
|
|
...brandEntries(AWS, [16509, 14618, 8987, 7224, 9059]),
|
|
...brandEntries(MICROSOFT, [8075, 8068, 8069, 8070]),
|
|
...brandEntries(YANDEX, [13238]),
|
|
...brandEntries(APPLE, [714, 6185]),
|
|
...brandEntries(DIGITALOCEAN, [14061]),
|
|
...brandEntries(HETZNER, [24940, 213230]),
|
|
...brandEntries(OVH, [16276]),
|
|
...brandEntries(ORACLE, [31898]),
|
|
...brandEntries(LINODE, [63949]),
|
|
...brandEntries(VULTR, [20473]),
|
|
...brandEntries(SCALEWAY, [12876]),
|
|
...brandEntries(IBM_CLOUD, [36351]),
|
|
...brandEntries(ALIBABA, [45102]),
|
|
...brandEntries(TENCENT, [132203]),
|
|
...brandEntries(GCORE, [199524]),
|
|
...brandEntries(CDN77, [60068]),
|
|
...brandEntries(SELECTEL, [50340, 49505]),
|
|
...brandEntries(TIMEWEB, [9123]),
|
|
...brandEntries(BEGET, [198610]),
|
|
...brandEntries(DDOS_GUARD, [57724]),
|
|
...brandEntries(META, [32934, 63293, 54115]),
|
|
...brandEntries(GOOGLE, [15169, 396982]),
|
|
...brandEntries(GITHUB, [36459]),
|
|
...brandEntries(GITLAB, [54876]),
|
|
...brandEntries(X, [13414]),
|
|
...brandEntries(LINKEDIN, [14413, 40793]),
|
|
...brandEntries(VK, [47541, 47764]),
|
|
...brandEntries(REDDIT, [394706]),
|
|
...brandEntries(DROPBOX, [19679]),
|
|
...brandEntries(SNAP, [19750]),
|
|
...brandEntries(WIKIPEDIA, [14907]),
|
|
...brandEntries(PAYPAL, [17012, 26101]),
|
|
...brandEntries(SALESFORCE, [14340]),
|
|
...brandEntries(YOUTUBE, [36040, 43515]),
|
|
...brandEntries(NETFLIX, [2906, 40027]),
|
|
...brandEntries(TWITCH, [46489]),
|
|
...brandEntries(TIKTOK, [396986, 138699]),
|
|
...brandEntries(SPOTIFY, [8403, 34081]),
|
|
...brandEntries(STEAM, [32590]),
|
|
...brandEntries(BLIZZARD, [57976]),
|
|
...brandEntries(EPIC, [395701]),
|
|
...brandEntries(RIOT, [6507, 62830]),
|
|
...brandEntries(PLAYSTATION, [33353]),
|
|
...brandEntries(ROBLOX, [22697]),
|
|
...brandEntries(UBISOFT, [197922]),
|
|
...brandEntries(DISCORD, [49544, 394141]),
|
|
...brandEntries(TELEGRAM, [62041, 59930, 211157]),
|
|
...brandEntries(ZOOM, [30103]),
|
|
...brandEntries(CHATGPT, [401115, 400645]),
|
|
...brandEntries(QUAD9, [19281]),
|
|
...brandEntries(OPENDNS, [36692]),
|
|
])
|
|
|
|
const ASN_HQ_COUNTRY = new Map<number, string>([
|
|
...hqEntries("US", [
|
|
13335, 209242, 54113, 20940, 16625, 32787, 35994, 16702, 24319,
|
|
16509, 14618, 8987, 7224, 8075, 8068, 8069, 8070, 15169, 396982,
|
|
32590, 57976, 2906, 40027, 36040, 43515, 46489, 401115, 400645,
|
|
49544, 394141, 32934, 63293, 54115, 714, 6185, 36459, 54876, 14061,
|
|
31898, 63949, 20473, 36351, 13414, 14413, 40793, 394706, 19679, 19750,
|
|
14907, 17012, 26101, 14340, 30103, 36692, 395701, 6507, 62830, 33353, 22697,
|
|
]),
|
|
...hqEntries("IE", [9059]),
|
|
...hqEntries("SG", [138699]),
|
|
...hqEntries("DE", [24940, 213230]),
|
|
...hqEntries("FR", [16276, 12876, 197922]),
|
|
...hqEntries("CN", [45102, 132203]),
|
|
...hqEntries("LU", [199524]),
|
|
...hqEntries("CZ", [60068]),
|
|
...hqEntries("RU", [13238, 50340, 49505, 9123, 198610, 57724, 47541, 47764]),
|
|
...hqEntries("SE", [8403, 34081]),
|
|
...hqEntries("NL", [62041, 59930, 211157]),
|
|
...hqEntries("CH", [19281]),
|
|
])
|
|
|
|
const CIDR_BRANDS: Array<{ cidr: string; prefixLen: number; hit: BrandHit }> = [
|
|
{ cidr: "104.16.0.0/13", prefixLen: 13, hit: CLOUDFLARE },
|
|
{ cidr: "104.24.0.0/14", prefixLen: 14, hit: CLOUDFLARE },
|
|
{ cidr: "172.64.0.0/13", prefixLen: 13, hit: CLOUDFLARE },
|
|
{ cidr: "162.158.0.0/15", prefixLen: 15, hit: CLOUDFLARE },
|
|
{ cidr: "8.8.8.0/24", prefixLen: 24, hit: GOOGLE },
|
|
{ cidr: "8.8.4.0/24", prefixLen: 24, hit: GOOGLE },
|
|
{ cidr: "173.194.0.0/16", prefixLen: 16, hit: GOOGLE },
|
|
{ cidr: "172.217.0.0/16", prefixLen: 16, hit: GOOGLE },
|
|
{ cidr: "74.125.0.0/16", prefixLen: 16, hit: GOOGLE },
|
|
{ cidr: "142.250.0.0/15", prefixLen: 15, hit: GOOGLE },
|
|
{ cidr: "208.65.152.0/22", prefixLen: 22, hit: YOUTUBE },
|
|
{ cidr: "208.117.224.0/19", prefixLen: 19, hit: YOUTUBE },
|
|
].sort((a, b) => b.prefixLen - a.prefixLen)
|
|
|
|
const HOLDER_BRANDS: Array<{ re: RegExp; hit: BrandHit }> = [
|
|
{ re: /youtube/i, hit: YOUTUBE },
|
|
{ re: /valve|\bsteam\b/i, hit: STEAM },
|
|
{ re: /blizzard|battle.?net/i, hit: BLIZZARD },
|
|
{ re: /openai/i, hit: CHATGPT },
|
|
{ re: /riot games/i, hit: RIOT },
|
|
{ re: /epic games/i, hit: EPIC },
|
|
{ re: /\bapple\b/i, hit: APPLE },
|
|
{ re: /github/i, hit: GITHUB },
|
|
{ re: /spotify/i, hit: SPOTIFY },
|
|
{ re: /twitter|\bx corp\b/i, hit: X },
|
|
{ re: /dropbox/i, hit: DROPBOX },
|
|
{ re: /akamai/i, hit: AKAMAI },
|
|
]
|
|
|
|
const NON_ISO = new Set(["EU", "AP", "ZZ", "XX", "A1", "A2", "O1"])
|
|
|
|
const STEAM_ASN = 32590
|
|
|
|
export function isIsoCountry(code: string): boolean {
|
|
const c = String(code ?? "").trim().toUpperCase()
|
|
return /^[A-Z]{2}$/.test(c) && !NON_ISO.has(c)
|
|
}
|
|
|
|
export function normalizeIsoCountry(code: string): string {
|
|
const c = String(code ?? "").trim().toUpperCase()
|
|
return isIsoCountry(c) ? c : ""
|
|
}
|
|
|
|
/** `CLOUDFLARENET, US` → `US`. */
|
|
export function countryFromHolder(holder: string): string {
|
|
const m = String(holder ?? "").trim().match(/,\s*([A-Za-z]{2})\s*$/)
|
|
return m?.[1] ? normalizeIsoCountry(m[1]) : ""
|
|
}
|
|
|
|
export function countryForAsn(asn: number): string {
|
|
if (!asn) return ""
|
|
return ASN_HQ_COUNTRY.get(asn) ?? ""
|
|
}
|
|
|
|
export function resolveRipeCountry(country: string, asn: number, holder: string): string {
|
|
return normalizeIsoCountry(country) || countryFromHolder(holder) || countryForAsn(asn)
|
|
}
|
|
|
|
export function brandByAsn(asn: number): BrandHit | null {
|
|
if (!asn) return null
|
|
return ASN_BRANDS.get(asn) ?? null
|
|
}
|
|
|
|
export function brandByCidr(ip: string): BrandHit | null {
|
|
for (const row of CIDR_BRANDS) {
|
|
if (parseCidrV4(row.cidr) && ipInCidrV4(ip, row.cidr)) return row.hit
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function brandByHolder(holder: string): BrandHit | null {
|
|
const h = String(holder ?? "").trim()
|
|
if (!h) return null
|
|
for (const row of HOLDER_BRANDS) {
|
|
if (row.re.test(h)) return row.hit
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** Игровые порты Steam — только вместе с AS32590, никогда :80/:443. */
|
|
export function isSteamGamePort(proto: number, dstPort: number, srcPort: number): boolean {
|
|
if (proto !== 6 && proto !== 17) return false
|
|
const port = dstPort || srcPort
|
|
if (!port || port === 80 || port === 443) return false
|
|
if (port === 4380 || port === 3478) return true
|
|
return port >= 27000 && port <= 27100
|
|
}
|
|
|
|
export function lookupBrand(ip: string, asn: number): BrandHit | null {
|
|
return brandByCidr(ip) || brandByAsn(asn)
|
|
}
|
|
|
|
/**
|
|
* Cloudflare CIDR бьёт holder (витрина на CF не становится Steam).
|
|
* Holder (YouTube и др.) бьёт остальные CIDR/ASN.
|
|
* Порты Steam — только AS32590 и не выше Cloudflare CIDR.
|
|
*/
|
|
export function resolveFlowBrand(
|
|
ip: string,
|
|
asn: number,
|
|
holder: string,
|
|
proto = 0,
|
|
dstPort = 0,
|
|
srcPort = 0,
|
|
): BrandHit | null {
|
|
const cidrBrand = brandByCidr(ip)
|
|
if (cidrBrand?.service === "Cloudflare") return cidrBrand
|
|
const holderBrand = brandByHolder(holder)
|
|
if (holderBrand) return holderBrand
|
|
const fromLookup = cidrBrand || brandByAsn(asn)
|
|
if (fromLookup) return fromLookup
|
|
if (asn === STEAM_ASN && isSteamGamePort(proto, dstPort, srcPort)) return STEAM
|
|
return null
|
|
}
|
|
|
|
const SKIP_MAP_SERVICES = new Set([
|
|
OTHER_SERVICE,
|
|
"GRE",
|
|
"ESP",
|
|
"WireGuard",
|
|
"DNS",
|
|
"SSH",
|
|
"BGP",
|
|
])
|
|
|
|
const SKIP_MAP_CATEGORIES = new Set(["Туннель", "DNS", "SSH", "BGP"])
|
|
|
|
/** Именованный интернет-сервис для карты (не туннель и не «Прочее»). */
|
|
export function isNamedInternetService(service: string, category: string): boolean {
|
|
const s = service.trim()
|
|
const c = category.trim()
|
|
if (!s || SKIP_MAP_SERVICES.has(s) || SKIP_MAP_CATEGORIES.has(c)) return false
|
|
return true
|
|
}
|
|
|
|
export function mapServiceNodeId(label: string): string {
|
|
const slug = label
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
return `svc:${slug || "unknown"}`
|
|
}
|