From f725d02dea2a9cba1491d7b3c8c9aa98935b27d5 Mon Sep 17 00:00:00 2001 From: ale Date: Wed, 11 Feb 2026 23:58:59 +0100 Subject: [PATCH] add geoip Signed-off-by: ale --- README.md | 113 +++++++++++++++++++++++++++++++++++++- index.js | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 3 files changed, 263 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f09c276..f71c9fb 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ A Node.js-based network packet capture tool that captures packets from network i - 🎯 **Flexible filtering**: Filter by protocol (TCP/UDP/ICMP), ports, and port ranges - 🔒 **Promiscuous mode support**: Optionally capture all packets on the network segment - 📊 **Elasticsearch integration**: Automatic indexing with optimized mapping -- � **Failover cache**: In-memory cache for packets when Elasticsearch is unavailable -- �📝 **Content extraction**: Captures and indexes readable (ASCII) packet content +- � **Failover cache**: In-memory cache for packets when Elasticsearch is unavailable- 🌍 **GeoIP enrichment**: Automatic geolocation data for remote IP addresses- �📝 **Content extraction**: Captures and indexes readable (ASCII) packet content - 🚀 **Smart content handling**: Automatically skips large binary content while preserving packet metadata - 📈 **Real-time statistics**: Track capture performance and statistics - ⚙️ **Highly configurable**: Environment variables and config file support @@ -52,6 +51,8 @@ cd /path/to/netpcap npm install ``` +**Note**: The `geoip-lite` package will download a GeoIP database on first install. This may take a few moments. + 3. Copy the example environment file and configure: ```bash cp .env.example .env @@ -146,6 +147,52 @@ CACHE_CHECK_INTERVAL=5000 - If cache reaches maximum size, oldest documents are removed (FIFO) - On graceful shutdown (SIGINT/SIGTERM), the system attempts to flush all cached documents +## GeoIP Enrichment + +The application automatically enriches captured packets with geolocation data for remote IP addresses: + +**Features:** +- Automatically detects the server's public IP address +- Identifies private/local IP addresses (RFC 1918, link-local, etc.) +- Only enriches remote public IP addresses +- Uses local GeoIP database (no external API calls during capture) +- Adds geolocation data for both source and destination IPs + +**GeoIP Data Included:** +- Country code and name +- Region/state +- City +- Timezone +- Geographic coordinates (latitude/longitude) +- Geo-point data for Elasticsearch geo queries + +**How it works:** +1. On startup, the application detects its public IP using an external service (ipify.org) +2. For each packet, it identifies if source/destination IPs are remote +3. Remote IPs are enriched with GeoIP data from the local database +4. Private IPs, loopback, and the server's own IP are excluded + +**Example GeoIP document structure:** +```json +{ + "geoip_src": { + "country": "US", + "region": "CA", + "city": "San Francisco", + "timezone": "America/Los_Angeles", + "location": { + "lat": 37.7749, + "lon": -122.4194 + } + }, + "geoip_dst": { + "country": "DE", + "city": "Frankfurt", + ... + } +} +``` + ## Usage ### Basic Usage @@ -229,6 +276,18 @@ The tool creates an index with the following document structure: "ack_seq": 0, "window": 65535 }, + "geoip_dst": { + "country": "US", + "country_name": "US", + "region": "CA", + "city": "Mountain View", + "timezone": "America/Los_Angeles", + "location": { + "lat": 37.4056, + "lon": -122.0775 + }, + "coordinates": [-122.0775, 37.4056] + }, "content": "GET / HTTP/1.1\r\nHost: example.com\r\n", "content_length": 1024, "content_type": "binary" @@ -281,6 +340,52 @@ curl -X GET "localhost:9200/network-packets/_search?pretty" -H 'Content-Type: ap ' ``` +**Find packets from a specific country:** +```bash +curl -X GET "localhost:9200/network-packets/_search?pretty" -H 'Content-Type: application/json' -d' +{ + "query": { + "term": { + "geoip_src.country": "US" + } + } +} +' +``` + +**Find packets to/from a specific geographic area:** +```bash +curl -X GET "localhost:9200/network-packets/_search?pretty" -H 'Content-Type: application/json' -d' +{ + "query": { + "geo_distance": { + "distance": "100km", + "geoip_dst.location": { + "lat": 37.7749, + "lon": -122.4194 + } + } + } +} +' +``` + +**Aggregate packets by destination country:** +```bash +curl -X GET "localhost:9200/network-packets/_search?pretty" -H 'Content-Type: application/json' -d' +{ + "size": 0, + "aggs": { + "countries": { + "terms": { + "field": "geoip_dst.country" + } + } + } +} +' +``` + ## Performance Considerations - **Promiscuous mode** can generate high packet volumes on busy networks @@ -327,11 +432,13 @@ curl -X GET "localhost:9200" ⚠️ **Important Security Notes:** - This tool captures network traffic and may contain sensitive information +- GeoIP data reveals geographic locations of communication endpoints - Store Elasticsearch credentials securely - Restrict access to the Elasticsearch index - Be aware of privacy and legal implications when capturing network traffic - Use encryption for Elasticsearch connections in production -- Comply with applicable laws and regulations +- The public IP detection makes an external HTTPS call to ipify.org on startup +- Comply with applicable laws and regulations regarding network monitoring and data retention ## License diff --git a/index.js b/index.js index b0c4d4b..80567ed 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,8 @@ const decoders = require('cap').decoders; const PROTOCOL = decoders.PROTOCOL; const { Client } = require('@elastic/elasticsearch'); const os = require('os'); +const https = require('https'); +const geoip = require('geoip-lite'); const config = require('./config'); // Initialize Elasticsearch client @@ -25,6 +27,9 @@ let esAvailable = true; let lastESCheckTime = Date.now(); const ES_CHECK_INTERVAL = config.cache.checkInterval; +// Public IP detection +let publicIP = null; + // Statistics tracking const stats = { packetsProcessed: 0, @@ -120,6 +125,106 @@ function buildBPFFilter() { return filters.length > 0 ? filters.join(' and ') : ''; } +/** + * Get public IP address from external service + */ +async function getPublicIP() { + return new Promise((resolve, reject) => { + https.get('https://api.ipify.org?format=json', (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const json = JSON.parse(data); + resolve(json.ip); + } catch (error) { + reject(error); + } + }); + }).on('error', (error) => { + reject(error); + }); + }); +} + +/** + * Check if IP address is private/local + */ +function isPrivateIP(ip) { + if (!ip) return true; + + // IPv4 private ranges + const ipv4PrivateRanges = [ + /^10\./, // 10.0.0.0/8 + /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12 + /^192\.168\./, // 192.168.0.0/16 + /^127\./, // Loopback + /^169\.254\./, // Link-local + /^0\./ // Invalid + ]; + + // Check IPv4 private ranges + if (ipv4PrivateRanges.some(regex => regex.test(ip))) { + return true; + } + + // IPv6 local addresses + if (ip.includes(':')) { + if (ip.startsWith('fe80:') || // Link-local + ip.startsWith('fc00:') || // Unique local + ip.startsWith('fd00:') || // Unique local + ip === '::1') { // Loopback + return true; + } + } + + return false; +} + +/** + * Check if IP is remote (not local, not private, not our public IP) + */ +function isRemoteIP(ip) { + if (!ip || isPrivateIP(ip)) return false; + if (publicIP && ip === publicIP) return false; + return true; +} + +/** + * Get GeoIP information for an IP address + */ +function getGeoIPData(ip) { + if (!ip || !isRemoteIP(ip)) { + return null; + } + + try { + const geo = geoip.lookup(ip); + + if (!geo) return null; + + return { + country: geo.country || null, + country_name: geo.country || null, + region: geo.region || null, + city: geo.city || null, + timezone: geo.timezone || null, + location: geo.ll ? { + lat: geo.ll[0], + lon: geo.ll[1] + } : null, + coordinates: geo.ll ? [geo.ll[1], geo.ll[0]] : null // [lon, lat] for Elasticsearch + }; + } catch (error) { + logger.debug(`Failed to get GeoIP data for ${ip}:`, error.message); + return null; + } +} + /** * Validate and format IP address for Elasticsearch * Returns null if invalid @@ -484,6 +589,21 @@ async function processPacket(buffer, interfaceInfo) { } } + // Add GeoIP data for remote IPs + if (packet.ip && packet.ip.src && packet.ip.dst) { + // Get GeoIP for source IP + const srcGeoIP = getGeoIPData(packet.ip.src); + if (srcGeoIP) { + packet.geoip_src = srcGeoIP; + } + + // Get GeoIP for destination IP + const dstGeoIP = getGeoIPData(packet.ip.dst); + if (dstGeoIP) { + packet.geoip_dst = dstGeoIP; + } + } + // Index to Elasticsearch (with cache fallback) await indexDocument(packet); @@ -623,6 +743,28 @@ async function initializeElasticsearch() { protocol: { type: 'keyword' } } }, + geoip_src: { + properties: { + country: { type: 'keyword' }, + country_name: { type: 'keyword' }, + region: { type: 'keyword' }, + city: { type: 'keyword' }, + timezone: { type: 'keyword' }, + location: { type: 'geo_point' }, + coordinates: { type: 'geo_point' } + } + }, + geoip_dst: { + properties: { + country: { type: 'keyword' }, + country_name: { type: 'keyword' }, + region: { type: 'keyword' }, + city: { type: 'keyword' }, + timezone: { type: 'keyword' }, + location: { type: 'geo_point' }, + coordinates: { type: 'geo_point' } + } + }, content: { type: 'text' }, content_length: { type: 'integer' }, content_type: { type: 'keyword' } @@ -683,6 +825,15 @@ async function main() { process.exit(1); } + // Get public IP address for GeoIP filtering + try { + publicIP = await getPublicIP(); + logger.info(`Detected public IP: ${publicIP}`); + } catch (error) { + logger.warn('Failed to detect public IP address:', error.message); + logger.warn('GeoIP will be applied to all non-private IPs'); + } + // Determine interfaces to capture let interfaces = config.capture.interfaces; diff --git a/package.json b/package.json index 9f546d0..d4f2b89 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "private": true, "dependencies": { "@elastic/elasticsearch": "^8.11.0", - "cap": "^0.2.1" + "cap": "^0.2.1", + "geoip-lite": "^1.4.10" }, "engines": { "node": ">=14.0.0"