- Deterministic Classification: MOAS/HIJACK/LEAK type detection - Severity scoring: CRITICAL/HIGH/MEDIUM/LOW based on prefix length - Optional Ollama enrichment (qwen2.5:3b) for CRITICAL only (5s timeout) - PostgreSQL backend: hijack_events, webhook_subscriptions, webhook_deliveries - HMAC-SHA256 webhook signing with exponential backoff retry - Retry scheduler: node-cron job every 5 minutes - 6 API endpoints: POST/GET/DELETE webhooks, test delivery, list/resolve hijacks - 22 comprehensive tests (80%+ coverage) - Zero external API costs (deterministic + local Ollama only)
200 lines
6.0 KiB
JavaScript
200 lines
6.0 KiB
JavaScript
/**
|
|
* BGP Hijack Monitor
|
|
*
|
|
* Background job that monitors own ASNs for unexpected BGP origin changes.
|
|
* Runs periodically (every 6 hours via systemd timer) to detect potential hijacks.
|
|
*
|
|
* Usage:
|
|
* node bgp-hijack-monitor.js
|
|
*
|
|
* Environment Variables:
|
|
* - DB_HOST: PostgreSQL host (default: 192.168.178.82)
|
|
* - DB_PORT: PostgreSQL port (default: 5432)
|
|
* - DB_NAME: Database name (default: llm_gateway)
|
|
* - DB_USER: Database user (default: llm)
|
|
* - DB_PASSWORD: Database password (default: llm_secure_2026)
|
|
* - OWN_ASNS: Comma-separated list of ASNs to monitor (e.g., "13335,15169,32787")
|
|
* - FINDINGS_TABLE: Table to write findings to (default: findings)
|
|
*/
|
|
|
|
const pg = require('pg');
|
|
const pool = new pg.Pool({
|
|
host: process.env.DB_HOST || '192.168.178.82',
|
|
port: parseInt(process.env.DB_PORT || '5432'),
|
|
database: process.env.DB_NAME || 'llm_gateway',
|
|
user: process.env.DB_USER || 'llm',
|
|
password: process.env.DB_PASSWORD || 'llm_secure_2026',
|
|
max: 5,
|
|
idleTimeoutMillis: 10000,
|
|
connectionTimeoutMillis: 5000,
|
|
});
|
|
|
|
const OWN_ASNS = (process.env.OWN_ASNS || '13335,15169').split(',').map(a => a.trim());
|
|
const FINDINGS_TABLE = process.env.FINDINGS_TABLE || 'findings';
|
|
|
|
/**
|
|
* Get all prefixes announced by an ASN from BGP routes table
|
|
*/
|
|
async function getBaselinePrefixes(asn) {
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
SELECT DISTINCT prefix, origin_asn
|
|
FROM bgp_routes
|
|
WHERE origin_asn = $1
|
|
LIMIT 1000
|
|
`;
|
|
const result = await client.query(query, [asn]);
|
|
return result.rows;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current BGP routes for a set of prefixes
|
|
*/
|
|
async function getCurrentRoutes(prefixes) {
|
|
if (prefixes.length === 0) return [];
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const placeholders = prefixes.map((_, i) => `$${i + 1}`).join(',');
|
|
const query = `
|
|
SELECT prefix, origin_asn
|
|
FROM bgp_routes
|
|
WHERE prefix = ANY(ARRAY[${placeholders}]::CIDR[])
|
|
AND last_seen > NOW() - INTERVAL '1 hour'
|
|
`;
|
|
const result = await client.query(query, prefixes);
|
|
return result.rows;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for hijacks: unexpected origin ASNs for known prefixes
|
|
*/
|
|
async function checkForHijacks(asn) {
|
|
console.log(`[BGP Hijack Monitor] Checking ASN ${asn} for unexpected origin changes...`);
|
|
|
|
try {
|
|
// Get baseline (expected) prefixes for this ASN
|
|
const baseline = await getBaselinePrefixes(asn);
|
|
if (baseline.length === 0) {
|
|
console.log(`[BGP Hijack Monitor] No baseline prefixes found for ASN ${asn}`);
|
|
return;
|
|
}
|
|
|
|
console.log(`[BGP Hijack Monitor] Baseline: ${baseline.length} prefixes for AS${asn}`);
|
|
|
|
// Get current routes for these prefixes
|
|
const prefixList = baseline.map(r => r.prefix);
|
|
const current = await getCurrentRoutes(prefixList);
|
|
|
|
// Group current routes by prefix
|
|
const currentByPrefix = {};
|
|
current.forEach(r => {
|
|
if (!currentByPrefix[r.prefix]) {
|
|
currentByPrefix[r.prefix] = [];
|
|
}
|
|
currentByPrefix[r.prefix].push(r.origin_asn);
|
|
});
|
|
|
|
// Compare: detect unexpected origin ASNs
|
|
const hijackCandidates = [];
|
|
baseline.forEach(expectedRoute => {
|
|
const prefix = expectedRoute.prefix;
|
|
const expectedAsn = expectedRoute.origin_asn;
|
|
|
|
if (currentByPrefix[prefix]) {
|
|
const currentAsns = currentByPrefix[prefix];
|
|
|
|
// If the expected ASN is NOT in the current set of origin ASNs, it's a hijack
|
|
if (!currentAsns.includes(expectedAsn)) {
|
|
hijackCandidates.push({
|
|
prefix,
|
|
expectedAsn,
|
|
foundAsns: currentAsns,
|
|
});
|
|
}
|
|
|
|
// If there are MULTIPLE origin ASNs (not just the expected one), it's also suspicious (MOAS)
|
|
if (currentAsns.length > 1) {
|
|
hijackCandidates.push({
|
|
prefix,
|
|
expectedAsn,
|
|
foundAsns: currentAsns,
|
|
type: 'MOAS', // Multiple Origin ASNs
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
if (hijackCandidates.length > 0) {
|
|
console.log(`[BGP Hijack Monitor] ⚠️ DETECTED ${hijackCandidates.length} hijack candidates`);
|
|
|
|
// Log each finding to the findings table
|
|
const client = await pool.connect();
|
|
try {
|
|
for (const hijack of hijackCandidates) {
|
|
const description = hijack.type === 'MOAS'
|
|
? `MOAS detected: Multiple origin ASNs (${hijack.foundAsns.join(', ')}) announcing ${hijack.prefix}`
|
|
: `BGP Hijack: AS${hijack.expectedAsn} expected but AS${hijack.foundAsns.join(', AS')} found announcing ${hijack.prefix}`;
|
|
|
|
const findingQuery = `
|
|
INSERT INTO ${FINDINGS_TABLE}
|
|
(timestamp, source, severity, asn, description, details)
|
|
VALUES (NOW(), 'bgp_hijack_monitor', 'HIGH', $1, $2, $3)
|
|
ON CONFLICT DO NOTHING
|
|
`;
|
|
|
|
const details = JSON.stringify({
|
|
prefix: hijack.prefix,
|
|
expected_asn: hijack.expectedAsn,
|
|
found_asns: hijack.foundAsns,
|
|
type: hijack.type || 'HIJACK',
|
|
detected_at: new Date().toISOString(),
|
|
});
|
|
|
|
await client.query(findingQuery, [asn, description, details]);
|
|
}
|
|
} finally {
|
|
client.release();
|
|
}
|
|
} else {
|
|
console.log(`[BGP Hijack Monitor] ✓ No hijacks detected for AS${asn}`);
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error(`[BGP Hijack Monitor] Error checking ASN ${asn}:`, err.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main: Check all monitored ASNs
|
|
*/
|
|
async function main() {
|
|
console.log(`[BGP Hijack Monitor] Starting hijack detection for ASNs: ${OWN_ASNS.join(', ')}`);
|
|
|
|
for (const asn of OWN_ASNS) {
|
|
await checkForHijacks(asn);
|
|
}
|
|
|
|
console.log('[BGP Hijack Monitor] Hijack detection complete');
|
|
await pool.end();
|
|
process.exit(0);
|
|
}
|
|
|
|
// Error handling
|
|
process.on('error', (err) => {
|
|
console.error('[BGP Hijack Monitor] Fatal error:', err);
|
|
process.exit(1);
|
|
});
|
|
|
|
main().catch(err => {
|
|
console.error('[BGP Hijack Monitor] Main loop error:', err);
|
|
process.exit(1);
|
|
});
|