113
README.md
113
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
|
- 🎯 **Flexible filtering**: Filter by protocol (TCP/UDP/ICMP), ports, and port ranges
|
||||||
- 🔒 **Promiscuous mode support**: Optionally capture all packets on the network segment
|
- 🔒 **Promiscuous mode support**: Optionally capture all packets on the network segment
|
||||||
- 📊 **Elasticsearch integration**: Automatic indexing with optimized mapping
|
- 📊 **Elasticsearch integration**: Automatic indexing with optimized mapping
|
||||||
- <20> **Failover cache**: In-memory cache for packets when Elasticsearch is unavailable
|
- <20> **Failover cache**: In-memory cache for packets when Elasticsearch is unavailable- 🌍 **GeoIP enrichment**: Automatic geolocation data for remote IP addresses- <20>📝 **Content extraction**: Captures and indexes readable (ASCII) packet content
|
||||||
- <20>📝 **Content extraction**: Captures and indexes readable (ASCII) packet content
|
|
||||||
- 🚀 **Smart content handling**: Automatically skips large binary content while preserving packet metadata
|
- 🚀 **Smart content handling**: Automatically skips large binary content while preserving packet metadata
|
||||||
- 📈 **Real-time statistics**: Track capture performance and statistics
|
- 📈 **Real-time statistics**: Track capture performance and statistics
|
||||||
- ⚙️ **Highly configurable**: Environment variables and config file support
|
- ⚙️ **Highly configurable**: Environment variables and config file support
|
||||||
@@ -52,6 +51,8 @@ cd /path/to/netpcap
|
|||||||
npm install
|
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:
|
3. Copy the example environment file and configure:
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
@@ -146,6 +147,52 @@ CACHE_CHECK_INTERVAL=5000
|
|||||||
- If cache reaches maximum size, oldest documents are removed (FIFO)
|
- If cache reaches maximum size, oldest documents are removed (FIFO)
|
||||||
- On graceful shutdown (SIGINT/SIGTERM), the system attempts to flush all cached documents
|
- 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
|
## Usage
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
@@ -229,6 +276,18 @@ The tool creates an index with the following document structure:
|
|||||||
"ack_seq": 0,
|
"ack_seq": 0,
|
||||||
"window": 65535
|
"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": "GET / HTTP/1.1\r\nHost: example.com\r\n",
|
||||||
"content_length": 1024,
|
"content_length": 1024,
|
||||||
"content_type": "binary"
|
"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
|
## Performance Considerations
|
||||||
|
|
||||||
- **Promiscuous mode** can generate high packet volumes on busy networks
|
- **Promiscuous mode** can generate high packet volumes on busy networks
|
||||||
@@ -327,11 +432,13 @@ curl -X GET "localhost:9200"
|
|||||||
⚠️ **Important Security Notes:**
|
⚠️ **Important Security Notes:**
|
||||||
|
|
||||||
- This tool captures network traffic and may contain sensitive information
|
- This tool captures network traffic and may contain sensitive information
|
||||||
|
- GeoIP data reveals geographic locations of communication endpoints
|
||||||
- Store Elasticsearch credentials securely
|
- Store Elasticsearch credentials securely
|
||||||
- Restrict access to the Elasticsearch index
|
- Restrict access to the Elasticsearch index
|
||||||
- Be aware of privacy and legal implications when capturing network traffic
|
- Be aware of privacy and legal implications when capturing network traffic
|
||||||
- Use encryption for Elasticsearch connections in production
|
- 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
|
## License
|
||||||
|
|
||||||
|
|||||||
151
index.js
151
index.js
@@ -10,6 +10,8 @@ const decoders = require('cap').decoders;
|
|||||||
const PROTOCOL = decoders.PROTOCOL;
|
const PROTOCOL = decoders.PROTOCOL;
|
||||||
const { Client } = require('@elastic/elasticsearch');
|
const { Client } = require('@elastic/elasticsearch');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const https = require('https');
|
||||||
|
const geoip = require('geoip-lite');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
|
||||||
// Initialize Elasticsearch client
|
// Initialize Elasticsearch client
|
||||||
@@ -25,6 +27,9 @@ let esAvailable = true;
|
|||||||
let lastESCheckTime = Date.now();
|
let lastESCheckTime = Date.now();
|
||||||
const ES_CHECK_INTERVAL = config.cache.checkInterval;
|
const ES_CHECK_INTERVAL = config.cache.checkInterval;
|
||||||
|
|
||||||
|
// Public IP detection
|
||||||
|
let publicIP = null;
|
||||||
|
|
||||||
// Statistics tracking
|
// Statistics tracking
|
||||||
const stats = {
|
const stats = {
|
||||||
packetsProcessed: 0,
|
packetsProcessed: 0,
|
||||||
@@ -120,6 +125,106 @@ function buildBPFFilter() {
|
|||||||
return filters.length > 0 ? filters.join(' and ') : '';
|
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
|
* Validate and format IP address for Elasticsearch
|
||||||
* Returns null if invalid
|
* 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)
|
// Index to Elasticsearch (with cache fallback)
|
||||||
await indexDocument(packet);
|
await indexDocument(packet);
|
||||||
|
|
||||||
@@ -623,6 +743,28 @@ async function initializeElasticsearch() {
|
|||||||
protocol: { type: 'keyword' }
|
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: { type: 'text' },
|
||||||
content_length: { type: 'integer' },
|
content_length: { type: 'integer' },
|
||||||
content_type: { type: 'keyword' }
|
content_type: { type: 'keyword' }
|
||||||
@@ -683,6 +825,15 @@ async function main() {
|
|||||||
process.exit(1);
|
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
|
// Determine interfaces to capture
|
||||||
let interfaces = config.capture.interfaces;
|
let interfaces = config.capture.interfaces;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elastic/elasticsearch": "^8.11.0",
|
"@elastic/elasticsearch": "^8.11.0",
|
||||||
"cap": "^0.2.1"
|
"cap": "^0.2.1",
|
||||||
|
"geoip-lite": "^1.4.10"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user