From b515d8c6a7f4ee8dcb5675608082bdd1dfe48a60 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 26 Mar 2026 15:35:22 +0000 Subject: [PATCH] fix(ipintelligence): handle flat geolocation MMDB schema and clean up DNS client lifecycle --- changelog.md | 7 ++++ test/test.ipintelligence.ts | 44 ++++++++++++----------- ts/00_commitinfo_data.ts | 2 +- ts/smartnetwork.classes.ipintelligence.ts | 43 +++++++++++++++------- 4 files changed, 63 insertions(+), 33 deletions(-) diff --git a/changelog.md b/changelog.md index 8dee49b..dcb3009 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-26 - 4.5.1 - fix(ipintelligence) +handle flat geolocation MMDB schema and clean up DNS client lifecycle + +- update IP intelligence lookups to read geolocation fields from the flat @ip-location-db schema instead of MaxMind nested city records +- destroy the Team Cymru DNS client after queries to avoid leaking resources +- adjust IP intelligence tests to validate countryCode-based geolocation and RDAP fields against current data sources + ## 2026-03-26 - 4.5.0 - feat(smartnetwork) add Rust-powered network diagnostics bridge and IP intelligence lookups diff --git a/test/test.ipintelligence.ts b/test/test.ipintelligence.ts index 227b4da..053ed93 100644 --- a/test/test.ipintelligence.ts +++ b/test/test.ipintelligence.ts @@ -8,6 +8,24 @@ tap.test('should create a SmartNetwork instance', async () => { expect(testSmartNetwork).toBeInstanceOf(smartnetwork.SmartNetwork); }); +tap.test('should get IP intelligence for 8.8.8.8 (Google)', async () => { + const result = await testSmartNetwork.getIpIntelligence('8.8.8.8'); + console.log('IP Intelligence for 8.8.8.8:', JSON.stringify(result, null, 2)); + + // Google's ASN is 15169 + expect(result.asn).toEqual(15169); + expect(result.asnOrg).toBeTruthy(); + + // Geolocation + expect(result.countryCode).toEqual('US'); + expect(result.latitude).not.toBeNull(); + expect(result.longitude).not.toBeNull(); + + // RDAP registration + expect(result.registrantOrg).toBeTruthy(); + expect(result.networkRange).toBeTruthy(); +}); + tap.test('should get IP intelligence for 1.1.1.1 (Cloudflare)', async () => { const result = await testSmartNetwork.getIpIntelligence('1.1.1.1'); console.log('IP Intelligence for 1.1.1.1:', JSON.stringify(result, null, 2)); @@ -16,24 +34,11 @@ tap.test('should get IP intelligence for 1.1.1.1 (Cloudflare)', async () => { expect(result.asn).toEqual(13335); expect(result.asnOrg).toBeTruthy(); - // Geolocation should be present - expect(result.country).toBeTruthy(); - expect(result.countryCode).toBeTruthy(); - expect(result.latitude).not.toBeNull(); - expect(result.longitude).not.toBeNull(); - // RDAP registration data should be present expect(result.networkRange).toBeTruthy(); -}); + expect(result.registrantCountry).toBeTruthy(); -tap.test('should get IP intelligence for 8.8.8.8 (Google)', async () => { - const result = await testSmartNetwork.getIpIntelligence('8.8.8.8'); - console.log('IP Intelligence for 8.8.8.8:', JSON.stringify(result, null, 2)); - - // Google's ASN is 15169 - expect(result.asn).toEqual(15169); - expect(result.country).toBeTruthy(); - expect(result.countryCode).toBeTruthy(); + // Note: 1.1.1.1 is anycast — city-level geo may be null in GeoLite2 }); tap.test('should get IP intelligence for own public IP', async () => { @@ -42,7 +47,7 @@ tap.test('should get IP intelligence for own public IP', async () => { const result = await testSmartNetwork.getIpIntelligence(ips.v4); console.log(`IP Intelligence for own IP (${ips.v4}):`, JSON.stringify(result, null, 2)); expect(result.asn).toBeTypeofNumber(); - expect(result.country).toBeTruthy(); + expect(result.countryCode).toBeTruthy(); } }); @@ -55,12 +60,11 @@ tap.test('should handle invalid IP gracefully', async () => { tap.test('should use cache when cacheTtl is set', async () => { const cached = new smartnetwork.SmartNetwork({ cacheTtl: 60000 }); - const r1 = await cached.getIpIntelligence('1.1.1.1'); - const r2 = await cached.getIpIntelligence('1.1.1.1'); + const r1 = await cached.getIpIntelligence('8.8.8.8'); + const r2 = await cached.getIpIntelligence('8.8.8.8'); // Second call should return the same cached result expect(r1.asn).toEqual(r2.asn); - expect(r1.country).toEqual(r2.country); - expect(r1.city).toEqual(r2.city); + expect(r1.countryCode).toEqual(r2.countryCode); }); export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8e41d2d..effa786 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartnetwork', - version: '4.5.0', + version: '4.5.1', description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.' } diff --git a/ts/smartnetwork.classes.ipintelligence.ts b/ts/smartnetwork.classes.ipintelligence.ts index 3ee97ee..60da9ae 100644 --- a/ts/smartnetwork.classes.ipintelligence.ts +++ b/ts/smartnetwork.classes.ipintelligence.ts @@ -2,7 +2,21 @@ import * as plugins from './smartnetwork.plugins.js'; import { getLogger } from './logging.js'; // MaxMind types re-exported from mmdb-lib via maxmind -import type { CityResponse, AsnResponse, Reader } from 'maxmind'; +import type { AsnResponse, Reader, Response } from 'maxmind'; + +/** + * The @ip-location-db MMDB files use a flat schema instead of the standard MaxMind nested format. + */ +interface IIpLocationDbCityRecord { + city?: string; + country_code?: string; + latitude?: number; + longitude?: number; + postcode?: string; + state1?: string; + state2?: string; + timezone?: string; +} /** * Unified result from all IP intelligence layers @@ -70,7 +84,7 @@ export class IpIntelligence { private readonly timeout: number; // MaxMind readers (lazily initialized) - private cityReader: Reader | null = null; + private cityReader: Reader | null = null; private asnReader: Reader | null = null; private lastFetchTime = 0; private refreshPromise: Promise | null = null; @@ -371,11 +385,12 @@ export class IpIntelligence { * Response: "ASN | prefix | CC | rir | date" */ private async queryTeamCymru(ip: string): Promise<{ asn: number; prefix: string; country: string } | null> { + let dnsClient: InstanceType | null = null; try { const reversed = ip.split('.').reverse().join('.'); const queryName = `${reversed}.origin.asn.cymru.com`; - const dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({ + dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({ strategy: 'prefer-system', allowDohFallback: true, timeoutMs: this.timeout, @@ -402,6 +417,10 @@ export class IpIntelligence { } catch (err: any) { this.logger.debug?.(`Team Cymru DNS query failed for ${ip}: ${err.message}`); return null; + } finally { + if (dnsClient) { + dnsClient.destroy(); + } } } @@ -442,7 +461,7 @@ export class IpIntelligence { this.fetchBuffer(ASN_MMDB_URL), ]); - this.cityReader = new plugins.maxmind.Reader(cityBuffer); + this.cityReader = new plugins.maxmind.Reader(cityBuffer); this.asnReader = new plugins.maxmind.Reader(asnBuffer); this.lastFetchTime = Date.now(); this.logger.info?.('MaxMind MMDB databases loaded into memory'); @@ -495,17 +514,17 @@ export class IpIntelligence { let asn: number | null = null; let asnOrg: string | null = null; - // City lookup + // City lookup — @ip-location-db uses flat schema: city, country_code, latitude, longitude, etc. try { const cityResult = this.cityReader.get(ip); if (cityResult) { - country = cityResult.country?.names?.en || null; - countryCode = cityResult.country?.iso_code || null; - city = cityResult.city?.names?.en || null; - latitude = cityResult.location?.latitude ?? null; - longitude = cityResult.location?.longitude ?? null; - accuracyRadius = cityResult.location?.accuracy_radius ?? null; - timezone = cityResult.location?.time_zone || null; + countryCode = cityResult.country_code || null; + city = cityResult.city || null; + latitude = cityResult.latitude ?? null; + longitude = cityResult.longitude ?? null; + timezone = cityResult.timezone || null; + // @ip-location-db does not include country name or accuracy_radius + // We leave country null (countryCode is available) } } catch (err: any) { this.logger.debug?.(`MaxMind city lookup failed for ${ip}: ${err.message}`);