/** * 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); });