feat: release v3.0.0 risk telemetry

This commit is contained in:
JetSprow
2026-04-29 18:30:49 +10:00
parent e109f6b246
commit a5f962db84
31 changed files with 1367 additions and 80 deletions

View File

@@ -79,7 +79,7 @@ function localizedName(record: { names?: object } | null | undefined) {
return names["zh-CN"] ?? names.en ?? Object.values(names).find((value) => typeof value === "string") ?? null;
}
function getGeoIpLocation(ip: string): RequestGeoContext {
export function getIpGeoContext(ip: string): RequestGeoContext {
if (ip === "unknown" || !isIP(ip)) return emptyGeoContext();
const reader = getGeoIpReader();
@@ -244,7 +244,7 @@ export function getRequestGeo(headers: HeaderReader, ip = "unknown"): RequestGeo
}
const headerGeo = { country, region, regionCode, city, latitude, longitude, source };
return mergeGeoContext(headerGeo, getGeoIpLocation(ip));
return mergeGeoContext(headerGeo, getIpGeoContext(ip));
}
export function getClientRequestContext(headers: HeaderReader): ClientRequestContext {

77
src/lib/route-classify.ts Normal file
View File

@@ -0,0 +1,77 @@
import { normalizeTraceText, type NormalizedTraceHop } from "@/lib/trace-normalize";
function normalizeAsn(value: unknown) {
const text = normalizeTraceText(value).toUpperCase();
const match = text.match(/(?:^|\b)AS?\s*(\d{2,10})(?:\b|$)/);
if (match) return match[1];
return /^\d{2,10}$/.test(text) ? text : "";
}
function isIpInPrefix(ip: string, prefix: string) {
return ip === prefix.slice(0, -1) || ip.startsWith(prefix);
}
function countMatches(values: string[], predicate: (value: string) => boolean) {
return values.reduce((count, value) => count + (predicate(value) ? 1 : 0), 0);
}
export function classifyTraceRoute(input: {
summary?: unknown;
hops: Array<NormalizedTraceHop | { ip?: unknown; geo?: unknown; asn?: unknown; owner?: unknown; isp?: unknown }>;
}) {
const summary = normalizeTraceText(input.summary).toUpperCase();
const hopTexts = input.hops.map((hop) => [
normalizeTraceText(hop.ip),
normalizeTraceText(hop.geo),
normalizeTraceText("owner" in hop ? hop.owner : ""),
normalizeTraceText("isp" in hop ? hop.isp : ""),
normalizeTraceText("asn" in hop ? hop.asn : ""),
].join(" ").toUpperCase());
const combined = [summary, ...hopTexts].join(" ");
const ips = input.hops.map((hop) => normalizeTraceText(hop.ip)).filter(Boolean);
const asns = new Set<string>();
for (const hop of input.hops) {
const directAsn = normalizeAsn("asn" in hop ? hop.asn : "");
if (directAsn) asns.add(directAsn);
}
for (const match of combined.matchAll(/(?:^|\b)AS\s*(\d{2,10})(?:\b|$)/g)) {
asns.add(match[1]);
}
const hasAsn = (...values: string[]) => values.some((value) => asns.has(value));
const hasText = (...values: string[]) => values.some((value) => combined.includes(value));
const hasIpPrefix = (...prefixes: string[]) => ips.some((ip) => prefixes.some((prefix) => isIpInPrefix(ip, prefix)));
const cn2Evidence = hasAsn("4809")
|| hasIpPrefix("59.43.")
|| hasText("CN2", "CTGNET", "CHINANET NEXT CARRYING NETWORK", "CHINA TELECOM GLOBAL");
const cn2GiaText = hasText("CN2 GIA", "CN2GIA", "GIA", "GLOBAL INTERNET ACCESS");
const ordinaryTelecomHops = countMatches(hopTexts, (text) => (
text.includes("AS4134")
|| text.includes("CHINANET BACKBONE")
|| text.includes("CHINANET 163")
|| text.includes("163骨干")
)) + countMatches(ips, (ip) => isIpInPrefix(ip, "202.97."));
if (cn2Evidence) {
if (cn2GiaText || ordinaryTelecomHops <= 1) return "CN2 GIA";
return "CN2 GT";
}
if (hasAsn("9929", "10099") || hasText("CUII", "A网", "AS9929")) {
return "AS9929";
}
if (hasText("CMIN2") || hasAsn("58807", "58809", "58813", "58819", "59807")) {
return "CMIN2";
}
if (hasText("CMI") || hasAsn("58453")) {
return "CMI";
}
if (hasAsn("4837") || hasText("AS4837")) {
return "AS4837";
}
const normalizedSummary = normalizeTraceText(input.summary);
return normalizedSummary && normalizedSummary !== "普通线路" ? normalizedSummary : "普通线路";
}

View File

@@ -77,6 +77,9 @@ export interface NormalizedTraceHop {
ip: string;
geo: string;
latency: number;
asn?: string;
owner?: string;
isp?: string;
}
export function normalizeTraceHops(hops: unknown): NormalizedTraceHop[] {
@@ -94,6 +97,9 @@ export function normalizeTraceHops(hops: unknown): NormalizedTraceHop[] {
ip: normalizeTraceText(hopObject.ip),
geo: normalizeTraceText(hopObject.geo),
latency: Math.max(0, toSafeNumber(hopObject.latency, 0)),
asn: normalizeTraceText(hopObject.asn) || undefined,
owner: normalizeTraceText(hopObject.owner) || undefined,
isp: normalizeTraceText(hopObject.isp) || undefined,
};
})
.filter((hop) => hop.ip || hop.geo || hop.latency > 0);