Add IP classification and categorization logic
Publish telemt-api gateway Docker image / test (push) Successful in 28s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m24s

- Introduced functions to classify IP addresses into categories (VPN, Hoster, ISP) based on the organization name, enhancing data organization.
- Implemented priority and labeling for categories to improve user interface clarity.
- Updated the data structure to include category information for each IP, allowing for better visualization and differentiation on the map.
- Enhanced tooltip display to show categorized information, improving user feedback and understanding of IP data.
This commit is contained in:
Denozordec
2026-03-30 16:24:15 +07:00
parent 7f7be8e98e
commit af11a49c81
+97 -3
View File
@@ -26,6 +26,81 @@
let mapStatus = $state<string | null>(null);
let renderAttempts = $state(0);
type IpCategory = 'vpn' | 'hoster' | 'isp' | 'unknown';
function classifyByAsOrg(asOrg: string | null | undefined): IpCategory {
const s = (asOrg ?? '').toLowerCase();
if (!s) return 'unknown';
// VPN heuristics from org name.
if (
s.includes('vpn') ||
s.includes('mullvad') ||
s.includes('nordvpn') ||
s.includes('proton') ||
s.includes('expressvpn') ||
s.includes('surfshark') ||
s.includes('cyberghost') ||
s.includes('strongvpn') ||
s.includes('ivpn') ||
s.includes('private internet access') ||
s.includes('pia ')
) {
return 'vpn';
}
// Hosting / cloud heuristics from org name.
if (
s.includes('cloudflare') ||
s.includes('akamai') ||
s.includes('fastly') ||
s.includes('ovh') ||
s.includes('hetzner') ||
s.includes('digitalocean') ||
s.includes('linode') ||
s.includes('vultr') ||
s.includes('amazon') ||
s.includes('aws') ||
s.includes('google') ||
s.includes('gcp') ||
s.includes('microsoft') ||
s.includes('azure') ||
s.includes('oracle') ||
s.includes('telegram') ||
s.includes('hosting') ||
s.includes('datacenter') ||
s.includes('data center') ||
s.includes('cdn') ||
s.includes('edge') ||
s.includes('compute')
) {
return 'hoster';
}
// If it doesn't match above, assume ISP.
return 'isp';
}
function categoryPriority(c: IpCategory): number {
if (c === 'vpn') return 3;
if (c === 'hoster') return 2;
if (c === 'isp') return 1;
return 0;
}
function categoryLabel(c: IpCategory): string {
if (c === 'vpn') return 'VPN';
if (c === 'hoster') return 'Hoster';
if (c === 'isp') return 'ISP';
return 'Unknown';
}
function truncate(s: string, n: number): string {
const x = s ?? '';
if (x.length <= n) return x;
return `${x.slice(0, n - 1)}…`;
}
async function ensureLeaflet() {
if (leaflet) return;
leaflet = await import('leaflet');
@@ -77,9 +152,11 @@
lat: number;
lon: number;
count: number;
category: IpCategory;
country_code?: string | null;
city_name?: string | null;
asn?: number | null;
as_organization?: string | null;
}
>();
@@ -97,6 +174,8 @@
const lonG = Math.round(lon * 10) / 10;
const country_code = ip.country_code ?? null;
const city_name = ip.city_name ?? null;
const asOrg = ip.as_organization ?? null;
const category = classifyByAsOrg(asOrg);
// Ключ включает country/city, чтобы не смешивать разные локации
// при одинаковых координатах округления.
@@ -104,6 +183,11 @@
const item = groups.get(key);
if (item) {
item.count++;
if (categoryPriority(category) > categoryPriority(item.category)) {
item.category = category;
item.asn = ip.asn ?? null;
item.as_organization = asOrg;
}
continue;
}
@@ -111,9 +195,11 @@
lat: latG,
lon: lonG,
count: 1,
category,
country_code,
city_name,
asn: ip.asn ?? null
asn: ip.asn ?? null,
as_organization: asOrg
});
}
}
@@ -141,12 +227,20 @@
for (const p of points) {
const r = Math.min(28, 5 + Math.log10(p.count + 1) * 9);
const color =
p.count >= 100 ? '#ef4444' : p.count >= 25 ? '#f97316' : p.count >= 5 ? '#22c55e' : '#60a5fa';
p.category === 'vpn'
? '#ef4444'
: p.category === 'hoster'
? '#f97316'
: p.category === 'isp'
? '#22c55e'
: '#60a5fa';
const tooltipParts = [
`${p.count} IP`,
categoryLabel(p.category),
p.country_code ? p.country_code : '—',
p.city_name ? p.city_name : ''
p.city_name ? p.city_name : '',
p.asn != null ? `AS${p.asn} ${p.as_organization ? truncate(p.as_organization, 24) : ''}` : ''
].filter(Boolean);
const marker = leaflet.circleMarker([p.lat, p.lon], {